LCOV - code coverage report
Current view: top level - dashql-pack/src - release_metadata.rs Coverage Total Hit
Test: DashQL 6e74948 Lines: 100.0 % 159 159
Test Date: 2026-11-06 17:46:50

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

Generated by: LCOV version 2.0-1