Skip to main content

backhopper_core/jigsaw/
releases.rs

1// Copyright (C) 2026 Michael S. Klishin and Contributors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5use std::slice;
6
7use thiserror::Error;
8
9use crate::model::non_empty::NonEmpty;
10use crate::model::release_version::{HostSeries, HostTag, HostVersion};
11
12pub trait HasHostTag {
13    fn host_tag(&self) -> &HostTag;
14}
15
16impl HasHostTag for HostTag {
17    fn host_tag(&self) -> &HostTag {
18        self
19    }
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Error)]
23pub enum SeriesReleasesError {
24    #[error("no releases")]
25    Empty,
26
27    #[error("release {0} is listed twice")]
28    Duplicate(HostVersion),
29
30    #[error("releases of series {first} and {other} are mixed")]
31    MixedSeries {
32        first: HostSeries,
33        other: HostSeries,
34    },
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct SeriesReleases<R>(NonEmpty<R>);
39
40impl<R: HasHostTag> SeriesReleases<R> {
41    pub fn new(mut releases: Vec<R>) -> Result<Self, SeriesReleasesError> {
42        releases.sort_by(|a, b| a.host_tag().version().cmp(b.host_tag().version()));
43        let releases = NonEmpty::try_from(releases).map_err(|_| SeriesReleasesError::Empty)?;
44        let series = releases.first().host_tag().version().series();
45        for pair in releases.as_slice().windows(2) {
46            let (earlier, later) = (pair[0].host_tag().version(), pair[1].host_tag().version());
47            if earlier == later {
48                return Err(SeriesReleasesError::Duplicate(later.clone()));
49            }
50        }
51        if let Some(other) = releases
52            .iter()
53            .map(|release| release.host_tag().version().series())
54            .find(|other| *other != series)
55        {
56            return Err(SeriesReleasesError::MixedSeries {
57                first: series,
58                other,
59            });
60        }
61        Ok(Self(releases))
62    }
63
64    #[must_use]
65    pub fn build(&self) -> &R {
66        self.0.last()
67    }
68
69    #[must_use]
70    pub fn first(&self) -> &R {
71        self.0.first()
72    }
73
74    #[must_use]
75    pub fn series(&self) -> HostSeries {
76        self.0.first().host_tag().version().series()
77    }
78
79    pub fn iter(&self) -> slice::Iter<'_, R> {
80        self.0.iter()
81    }
82
83    #[must_use]
84    pub fn as_slice(&self) -> &[R] {
85        self.0.as_slice()
86    }
87
88    /// The mapped value carries its own tag, so the order is checked again.
89    pub fn map<U: HasHostTag>(
90        self,
91        f: impl FnMut(R) -> U,
92    ) -> Result<SeriesReleases<U>, SeriesReleasesError> {
93        SeriesReleases::new(self.0.into_iter().map(f).collect())
94    }
95}
96
97impl<'a, R> IntoIterator for &'a SeriesReleases<R> {
98    type Item = &'a R;
99    type IntoIter = slice::Iter<'a, R>;
100
101    fn into_iter(self) -> Self::IntoIter {
102        self.0.iter()
103    }
104}