Skip to main content

update_rs/
release.rs

1use semver::Version;
2
3/// A single release published by a [`Source`](crate::Source), along with the
4/// binary (variant) selected for it by the source's configured asset selection.
5#[derive(Debug, Clone)]
6pub struct Release {
7    /// The release identifier, usually the Git tag it was published under
8    /// (e.g. `v1.2.3`). This is what gets embedded in the temporary file name
9    /// and used to construct download URLs.
10    pub id: String,
11    /// The release notes / changelog associated with the release.
12    pub changelog: String,
13    /// The parsed [semantic version](semver::Version) of the release, used to
14    /// determine which release is the newest.
15    pub version: Version,
16    /// Whether this release is a pre-release (beta/early-access) version.
17    pub prerelease: bool,
18    /// The release asset selected for this platform, if any. For
19    /// [`GitHubSource`](crate::GitHubSource) this is the asset whose name matches
20    /// the configured glob pattern, or `None` when the release has no matching
21    /// asset.
22    pub variant: Option<ReleaseVariant>,
23}
24
25impl Release {
26    /// The variant (asset) selected for this platform, if the release has one.
27    ///
28    /// The source has already resolved this to the asset matching its configured
29    /// selection. Use [`is_some`](Option::is_some) to check whether a release
30    /// has a usable binary before offering it as an update.
31    pub fn get_variant(&self) -> Option<&ReleaseVariant> {
32        self.variant.as_ref()
33    }
34
35    /// Return the release with the highest [version](Release::version) from an
36    /// iterator of releases, or `None` if the iterator is empty.
37    pub fn get_latest<'a, I>(releases: I) -> Option<&'a Self>
38    where
39        I: IntoIterator<Item = &'a Self>,
40    {
41        let mut latest: Option<&Self> = None;
42
43        for r in releases {
44            match latest {
45                Some(lr) if r.version > lr.version => latest = Some(r),
46                None => latest = Some(r),
47                _ => {}
48            }
49        }
50
51        latest
52    }
53}
54
55impl PartialEq<Release> for Release {
56    fn eq(&self, other: &Release) -> bool {
57        self.id == other.id
58    }
59}
60
61impl std::fmt::Display for Release {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self.prerelease {
64            false => write!(f, "{}", &self.id),
65            true => write!(f, "{}-beta", &self.id),
66        }
67    }
68}
69
70/// A downloadable binary belonging to a [`Release`] — a single release asset.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct ReleaseVariant {
73    /// The release asset's file name. For [`GitHubSource`](crate::GitHubSource)
74    /// this is used directly to construct the download URL.
75    pub name: String,
76    /// The expected SHA-256 digest of the asset as lowercase hex, if the source
77    /// provides one. When present, the source verifies the downloaded bytes
78    /// against it before the update proceeds.
79    pub sha256: Option<String>,
80}
81
82impl std::fmt::Display for ReleaseVariant {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        write!(f, "{}", &self.name)
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    fn variant(name: &str) -> ReleaseVariant {
93        ReleaseVariant {
94            name: name.to_string(),
95            sha256: None,
96        }
97    }
98
99    #[test]
100    fn test_get_latest() {
101        assert_eq!(Release::get_latest(vec![]), None);
102
103        let releases = [
104            Release {
105                id: "1".to_string(),
106                changelog: "".to_string(),
107                version: "1.1.7".parse().unwrap(),
108                prerelease: false,
109                variant: None,
110            },
111            Release {
112                id: "0".to_string(),
113                changelog: "".to_string(),
114                version: "1.0.0".parse().unwrap(),
115                prerelease: false,
116                variant: None,
117            },
118            Release {
119                id: "3".to_string(),
120                changelog: "".to_string(),
121                version: "2.3.1".parse().unwrap(),
122                prerelease: false,
123                variant: None,
124            },
125            Release {
126                id: "2".to_string(),
127                changelog: "".to_string(),
128                version: "2.1.0".parse().unwrap(),
129                prerelease: false,
130                variant: None,
131            },
132        ];
133
134        assert_eq!(
135            Release::get_latest(releases.iter()).map(|r| r.id.as_str()),
136            Some("3")
137        );
138    }
139
140    #[test]
141    fn test_get_variant() {
142        let with = Release {
143            id: "v1".to_string(),
144            changelog: "".to_string(),
145            version: "1.0.0".parse().unwrap(),
146            prerelease: false,
147            variant: Some(variant("myapp-linux-amd64")),
148        };
149        assert_eq!(with.get_variant(), Some(&variant("myapp-linux-amd64")));
150
151        let without = Release {
152            id: "v1".to_string(),
153            changelog: "".to_string(),
154            version: "1.0.0".parse().unwrap(),
155            prerelease: false,
156            variant: None,
157        };
158        assert_eq!(without.get_variant(), None);
159    }
160}