Line data Source code
1 : use std::collections::HashMap;
2 :
3 : use crate::serde_date;
4 : use crate::serde_version;
5 : use anyhow::Result;
6 : use chrono::prelude::*;
7 : use semver::Version;
8 : use serde::{Deserialize, Deserializer, Serialize, Serializer};
9 :
10 : #[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
11 : pub enum Platform {
12 : Darwin,
13 : Linux,
14 : Windows,
15 : }
16 :
17 : #[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
18 : pub enum Architecture {
19 : X86_64,
20 : Aarch64,
21 : }
22 :
23 : #[derive(Eq, PartialEq, Hash, Serialize, Deserialize, Debug)]
24 : pub enum BundleType {
25 : Dmg,
26 : App,
27 : AppImage,
28 : Nsis,
29 : }
30 :
31 : /// A platform and architecture a bundle runs on, serialized as `<platform>-<arch>`
32 : #[derive(Hash, Eq, PartialEq, Debug)]
33 : pub struct BundleTarget {
34 : /// The operating system
35 : pub platform: Platform,
36 : /// The CPU architecture
37 : pub arch: Architecture,
38 : }
39 :
40 : impl Serialize for BundleTarget {
41 44 : fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
42 44 : where
43 44 : S: Serializer,
44 : {
45 44 : let arch = match self.arch {
46 22 : Architecture::X86_64 => "x86_64",
47 22 : Architecture::Aarch64 => "aarch64",
48 : };
49 44 : let platform = match self.platform {
50 40 : Platform::Darwin => "darwin",
51 2 : Platform::Windows => "windows",
52 2 : Platform::Linux => "linux",
53 : };
54 44 : serializer.serialize_str(&format!("{}-{}", platform, arch))
55 44 : }
56 : }
57 : impl<'d> Deserialize<'d> for BundleTarget {
58 15 : fn deserialize<D>(deserializer: D) -> Result<BundleTarget, D::Error>
59 15 : where
60 15 : D: Deserializer<'d>,
61 : {
62 15 : let s = String::deserialize(deserializer)?;
63 15 : let invalid_value_error = |s: &str| {
64 2 : Err(serde::de::Error::invalid_value(
65 2 : serde::de::Unexpected::Str(s),
66 2 : &"{platform}_arch",
67 2 : ))
68 2 : };
69 15 : let parts: Vec<&str> = s.split("-").collect();
70 15 : if parts.len() != 2 {
71 2 : return invalid_value_error(&s);
72 13 : }
73 13 : let platform = match parts[0] {
74 13 : "darwin" => Platform::Darwin,
75 7 : "linux" => Platform::Linux,
76 4 : "windows" => Platform::Windows,
77 : _ => {
78 1 : return Err(serde::de::Error::invalid_value(
79 1 : serde::de::Unexpected::Str(&s),
80 1 : &"darwin",
81 1 : ))
82 : }
83 : };
84 12 : let arch = match parts[1] {
85 12 : "x86_64" => Architecture::X86_64,
86 5 : "aarch64" => Architecture::Aarch64,
87 : _ => {
88 1 : return Err(serde::de::Error::invalid_value(
89 1 : serde::de::Unexpected::Str(&s),
90 1 : &"x86_64 or aarch64",
91 1 : ))
92 : }
93 : };
94 11 : Ok(BundleTarget { platform, arch })
95 15 : }
96 : }
97 :
98 : /// An update artifact of a single platform in an [`UpdateManifest`]
99 : #[derive(Serialize, Deserialize, Clone, Debug)]
100 : pub struct UpdateArtifact {
101 : /// The public URL of the update ZIP
102 : pub url: String,
103 : /// The signature of the update artifact, currently empty
104 : pub signature: String,
105 : }
106 :
107 : /// The update manifest of a release, listing the update artifact per platform
108 : #[derive(Serialize, Deserialize, Debug)]
109 : pub struct UpdateManifest {
110 : /// The release version
111 : #[serde(with = "serde_version")]
112 : pub version: Version,
113 : /// The release notes, currently empty
114 : pub notes: String,
115 : /// The publication date
116 : #[serde(with = "serde_date")]
117 : pub pub_date: DateTime<Utc>,
118 : /// The update artifacts, keyed by target platform
119 : pub platforms: HashMap<BundleTarget, UpdateArtifact>,
120 : }
121 :
122 : impl Default for UpdateManifest {
123 18 : fn default() -> Self {
124 18 : Self {
125 18 : version: Version::new(0, 0, 0),
126 18 : notes: String::default(),
127 18 : pub_date: DateTime::default(),
128 18 : platforms: HashMap::default(),
129 18 : }
130 18 : }
131 : }
132 :
133 : /// A downloadable installer bundle of a release
134 : #[derive(Serialize, Deserialize, Debug)]
135 : pub struct Bundle {
136 : /// The public URL of the bundle
137 : pub url: String,
138 : /// The signature of the bundle, if signed
139 : pub signature: Option<String>,
140 : /// The file name of the bundle
141 : pub name: String,
142 : /// The bundle format
143 : pub bundle_type: BundleType,
144 : /// The platforms the bundle runs on
145 : pub targets: Vec<BundleTarget>,
146 : }
147 :
148 : /// The metadata of a release, uploaded next to its artifacts and to the channel pointers
149 : #[derive(Serialize, Deserialize, Debug)]
150 : pub struct ReleaseMetadata {
151 : /// The release id, a UUID derived from the version
152 : pub release_id: String,
153 : /// The publication date
154 : #[serde(with = "serde_date")]
155 : pub pub_date: DateTime<Utc>,
156 : /// The release version
157 : #[serde(with = "serde_version")]
158 : pub version: Version,
159 : /// The abbreviated hash of the released commit
160 : pub git_commit_hash: String,
161 : /// The GitHub URL of the released commit
162 : pub git_commit_url: String,
163 : /// The public URL of the release update manifest
164 : pub update_manifest_url: String,
165 : /// The downloadable bundles of the release
166 : pub bundles: Vec<Bundle>,
167 : }
168 :
169 : /// A summary of a published release, optionally written by the publish command
170 : #[derive(Serialize, Deserialize, Debug)]
171 : pub struct ReleaseSummary {
172 : /// The release id, a UUID derived from the version
173 : pub release_id: String,
174 : /// The release version
175 : #[serde(with = "serde_version")]
176 : pub version: Version,
177 : /// The publication date
178 : #[serde(with = "serde_date")]
179 : pub pub_date: DateTime<Utc>,
180 : /// The abbreviated hash of the released commit
181 : pub git_commit_hash: String,
182 : /// The GitHub URL of the released commit
183 : pub git_commit_url: String,
184 : /// The public URL of the release metadata
185 : pub release_metadata_url: String,
186 : /// The public URL of the release update manifest
187 : pub update_manifest_url: String,
188 : /// The public URL of the first macOS DMG bundle
189 : pub bundle_macos_dmg_url: String,
190 : }
191 :
192 : impl Default for ReleaseMetadata {
193 19 : fn default() -> Self {
194 19 : Self {
195 19 : release_id: String::default(),
196 19 : pub_date: DateTime::default(),
197 19 : version: Version::new(0, 0, 0),
198 19 : git_commit_hash: String::default(),
199 19 : git_commit_url: String::default(),
200 19 : update_manifest_url: String::default(),
201 19 : bundles: Vec::default(),
202 19 : }
203 19 : }
204 : }
205 :
206 : #[cfg(test)]
207 : mod tests {
208 : use super::*;
209 : #[test]
210 1 : fn test_update_manifest() -> Result<()> {
211 1 : let data = r#"
212 1 : {
213 1 : "version": "1.0.0",
214 1 : "notes": "Test version",
215 1 : "pub_date": "2020-06-22T19:25:57Z",
216 1 : "platforms": {
217 1 : "darwin-x86_64": {
218 1 : "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-x86_64.app.tar.gz",
219 1 : "signature": "Content of app.tar.gz.sig"
220 1 : },
221 1 : "darwin-aarch64": {
222 1 : "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-aarch64.app.tar.gz",
223 1 : "signature": "Content of app.tar.gz.sig"
224 1 : },
225 1 : "linux-x86_64": {
226 1 : "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-amd64.AppImage.tar.gz",
227 1 : "signature": "Content of app.AppImage.tar.gz.sig"
228 1 : },
229 1 : "windows-x86_64": {
230 1 : "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-x64-setup.nsis.zip",
231 1 : "signature": "Content of app-setup.nsis.sig or app.msi.sig, depending on the chosen format"
232 1 : }
233 1 : }
234 1 : }
235 1 : "#;
236 :
237 : // Parse the string of data into serde_json::Value.
238 1 : let v: UpdateManifest = serde_json::from_str(data)?;
239 1 : assert_eq!(v.version.major, 1);
240 1 : assert_eq!(v.version.minor, 0);
241 1 : assert_eq!(v.version.patch, 0);
242 1 : assert_eq!(v.notes, "Test version");
243 1 : assert_eq!(v.platforms.len(), 4);
244 1 : assert!(v.platforms.contains_key(&BundleTarget {
245 1 : platform: Platform::Darwin,
246 1 : arch: Architecture::X86_64,
247 1 : }));
248 1 : assert!(v.platforms.contains_key(&BundleTarget {
249 1 : platform: Platform::Darwin,
250 1 : arch: Architecture::Aarch64,
251 1 : }));
252 1 : assert!(v.platforms.contains_key(&BundleTarget {
253 1 : platform: Platform::Linux,
254 1 : arch: Architecture::X86_64,
255 1 : }));
256 1 : assert!(v.platforms.contains_key(&BundleTarget {
257 1 : platform: Platform::Windows,
258 1 : arch: Architecture::X86_64,
259 1 : }));
260 1 : let darwin_x64 = &v
261 1 : .platforms
262 1 : .get(&BundleTarget {
263 1 : platform: Platform::Darwin,
264 1 : arch: Architecture::X86_64,
265 1 : })
266 1 : .unwrap();
267 1 : assert_eq!(
268 : darwin_x64.url,
269 : "https://github.com/username/reponame/releases/download/v1.0.0/app-x86_64.app.tar.gz"
270 : );
271 1 : assert_eq!(darwin_x64.signature, "Content of app.tar.gz.sig");
272 :
273 1 : Ok(())
274 1 : }
275 :
276 : #[test]
277 1 : fn bundle_targets_keep_their_names() -> Result<()> {
278 3 : for platform in [Platform::Darwin, Platform::Linux, Platform::Windows] {
279 6 : for arch in [Architecture::X86_64, Architecture::Aarch64] {
280 6 : let target = BundleTarget { platform, arch };
281 6 : let text = serde_json::to_string(&target)?;
282 6 : assert_eq!(serde_json::from_str::<BundleTarget>(&text)?, target);
283 : }
284 : }
285 1 : let target = BundleTarget { platform: Platform::Darwin, arch: Architecture::Aarch64 };
286 1 : assert_eq!(serde_json::to_string(&target)?, "\"darwin-aarch64\"");
287 1 : Ok(())
288 1 : }
289 :
290 : #[test]
291 1 : fn rejects_unknown_bundle_targets() {
292 4 : for text in ["\"darwin\"", "\"darwin-x86_64-extra\"", "\"beos-x86_64\"", "\"darwin-riscv\""] {
293 4 : assert!(serde_json::from_str::<BundleTarget>(text).is_err(), "{text}");
294 : }
295 1 : }
296 :
297 : #[test]
298 1 : fn metadata_round_trips_with_its_date_and_version() -> Result<()> {
299 1 : let mut metadata = ReleaseMetadata::default();
300 1 : metadata.version = Version::parse("1.2.4-dev.3")?;
301 1 : metadata.pub_date = Utc.with_ymd_and_hms(2026, 1, 2, 3, 4, 5).unwrap();
302 1 : metadata.bundles.push(Bundle {
303 1 : url: "https://get.example/a.dmg".to_string(),
304 1 : signature: None,
305 1 : name: "a.dmg".to_string(),
306 1 : bundle_type: BundleType::Dmg,
307 1 : targets: vec![BundleTarget { platform: Platform::Darwin, arch: Architecture::X86_64 }],
308 1 : });
309 1 : let text = serde_json::to_string(&metadata)?;
310 1 : assert!(text.contains("\"version\":\"1.2.4-dev.3\""), "{text}");
311 1 : assert!(text.contains("\"pub_date\":\"2026-01-02T03:04:05Z\""), "{text}");
312 1 : let parsed: ReleaseMetadata = serde_json::from_str(&text)?;
313 1 : assert_eq!(parsed.version, metadata.version);
314 1 : assert_eq!(parsed.pub_date, metadata.pub_date);
315 1 : assert_eq!(parsed.bundles[0].targets[0], metadata.bundles[0].targets[0]);
316 1 : Ok(())
317 1 : }
318 :
319 : #[test]
320 1 : fn rejects_a_date_of_another_format() {
321 1 : let manifest = r#"{ "version": "1.0.0", "notes": "", "pub_date": "2026-01-02", "platforms": {} }"#;
322 1 : assert!(serde_json::from_str::<UpdateManifest>(manifest).is_err());
323 1 : }
324 : }
|