debian_watch/
release.rs

1//! Types for representing discovered releases.
2
3use debversion::Version;
4use std::cmp::Ordering;
5
6/// A discovered release from an upstream source
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct Release {
9    /// The version string of the release (after uversionmangle)
10    pub version: String,
11    /// The URL to download the release tarball (after downloadurlmangle)
12    pub url: String,
13    /// Optional URL to the PGP signature file
14    pub pgpsigurl: Option<String>,
15    /// Optional target filename for the downloaded tarball (from filenamemangle)
16    pub target_filename: Option<String>,
17    /// Optional Debian package version (from oversionmangle, e.g., "1.0+dfsg")
18    pub package_version: Option<String>,
19}
20
21impl Release {
22    /// Create a new Release
23    ///
24    /// # Examples
25    ///
26    /// ```
27    /// use debian_watch::Release;
28    ///
29    /// let release = Release::new("1.0.0", "https://example.com/project-1.0.0.tar.gz", None);
30    /// assert_eq!(release.version, "1.0.0");
31    /// assert_eq!(release.url, "https://example.com/project-1.0.0.tar.gz");
32    /// ```
33    pub fn new(
34        version: impl Into<String>,
35        url: impl Into<String>,
36        pgpsigurl: Option<String>,
37    ) -> Self {
38        Self {
39            version: version.into(),
40            url: url.into(),
41            pgpsigurl,
42            target_filename: None,
43            package_version: None,
44        }
45    }
46
47    /// Create a new Release with all fields
48    ///
49    /// # Examples
50    ///
51    /// ```
52    /// use debian_watch::Release;
53    ///
54    /// let release = Release::new_full(
55    ///     "1.0.0",
56    ///     "https://example.com/project-1.0.0.tar.gz",
57    ///     Some("https://example.com/project-1.0.0.tar.gz.asc".to_string()),
58    ///     Some("myproject_1.0.0.orig.tar.gz".to_string()),
59    ///     Some("1.0.0+dfsg".to_string()),
60    /// );
61    /// assert_eq!(release.version, "1.0.0");
62    /// assert_eq!(release.target_filename, Some("myproject_1.0.0.orig.tar.gz".to_string()));
63    /// ```
64    pub fn new_full(
65        version: impl Into<String>,
66        url: impl Into<String>,
67        pgpsigurl: Option<String>,
68        target_filename: Option<String>,
69        package_version: Option<String>,
70    ) -> Self {
71        Self {
72            version: version.into(),
73            url: url.into(),
74            pgpsigurl,
75            target_filename,
76            package_version,
77        }
78    }
79
80    /// Download the release tarball (async version)
81    ///
82    /// Downloads the tarball from the release URL.
83    /// Requires the 'discover' feature.
84    ///
85    /// # Examples
86    ///
87    /// ```ignore
88    /// use debian_watch::Release;
89    ///
90    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
91    /// let release = Release::new("1.0.0", "https://example.com/project-1.0.tar.gz", None);
92    /// let data = release.download().await?;
93    /// println!("Downloaded {} bytes", data.len());
94    /// # Ok(())
95    /// # }
96    /// ```
97    #[cfg(feature = "discover")]
98    pub async fn download(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
99        let client = reqwest::Client::new();
100        let response = client.get(&self.url).send().await?;
101        let bytes = response.bytes().await?;
102        Ok(bytes.to_vec())
103    }
104
105    /// Download the release tarball (blocking version)
106    ///
107    /// Downloads the tarball from the release URL.
108    /// Requires both 'discover' and 'blocking' features.
109    ///
110    /// # Examples
111    ///
112    /// ```ignore
113    /// use debian_watch::Release;
114    ///
115    /// let release = Release::new("1.0.0", "https://example.com/project-1.0.tar.gz", None);
116    /// let data = release.download_blocking()?;
117    /// println!("Downloaded {} bytes", data.len());
118    /// ```
119    #[cfg(all(feature = "discover", feature = "blocking"))]
120    pub fn download_blocking(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
121        let client = reqwest::blocking::Client::new();
122        let response = client.get(&self.url).send()?;
123        let bytes = response.bytes()?;
124        Ok(bytes.to_vec())
125    }
126}
127
128impl PartialOrd for Release {
129    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
130        Some(self.cmp(other))
131    }
132}
133
134impl Ord for Release {
135    fn cmp(&self, other: &Self) -> Ordering {
136        // Parse versions and compare them
137        match (
138            self.version.parse::<Version>(),
139            other.version.parse::<Version>(),
140        ) {
141            (Ok(v1), Ok(v2)) => v1.cmp(&v2),
142            // If parsing fails, fall back to string comparison
143            _ => self.version.cmp(&other.version),
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_release_new() {
154        let release = Release::new("1.0.0", "https://example.com/foo.tar.gz", None);
155        assert_eq!(release.version, "1.0.0");
156        assert_eq!(release.url, "https://example.com/foo.tar.gz");
157        assert_eq!(release.pgpsigurl, None);
158
159        let release = Release::new(
160            "2.0.0",
161            "https://example.com/foo-2.0.0.tar.gz",
162            Some("https://example.com/foo-2.0.0.tar.gz.asc".to_string()),
163        );
164        assert_eq!(release.version, "2.0.0");
165        assert_eq!(
166            release.pgpsigurl,
167            Some("https://example.com/foo-2.0.0.tar.gz.asc".to_string())
168        );
169    }
170
171    #[test]
172    fn test_release_ordering() {
173        let r1 = Release::new("1.0.0", "https://example.com/foo-1.0.0.tar.gz", None);
174        let r2 = Release::new("2.0.0", "https://example.com/foo-2.0.0.tar.gz", None);
175        let r3 = Release::new("1.5.0", "https://example.com/foo-1.5.0.tar.gz", None);
176
177        assert!(r1 < r2);
178        assert!(r2 > r1);
179        assert!(r1 < r3);
180        assert!(r3 < r2);
181    }
182
183    #[test]
184    fn test_release_ordering_debian_versions() {
185        // Test with Debian version strings
186        let r1 = Release::new("1.0", "https://example.com/foo-1.0.tar.gz", None);
187        let r2 = Release::new("1.0+dfsg", "https://example.com/foo-1.0+dfsg.tar.gz", None);
188        let r3 = Release::new("1.0~rc1", "https://example.com/foo-1.0~rc1.tar.gz", None);
189
190        // 1.0~rc1 < 1.0 < 1.0+dfsg in Debian version ordering
191        assert!(r3 < r1);
192        assert!(r1 < r2);
193    }
194
195    #[test]
196    fn test_release_max() {
197        let releases = vec![
198            Release::new("1.0.0", "https://example.com/foo-1.0.0.tar.gz", None),
199            Release::new("2.0.0", "https://example.com/foo-2.0.0.tar.gz", None),
200            Release::new("1.5.0", "https://example.com/foo-1.5.0.tar.gz", None),
201        ];
202
203        let max = releases.iter().max().unwrap();
204        assert_eq!(max.version, "2.0.0");
205    }
206}