dropshot_api_manager_types/
versions.rs

1// Copyright 2025 Oxide Computer Company
2
3//! Types used by trait-based API definitions to define the versions that they
4//! support.
5
6use std::collections::BTreeMap;
7
8/// Describes how an API is versioned
9#[derive(Clone, Debug)]
10pub enum Versions {
11    /// There is only ever one version of this API
12    ///
13    /// Clients and servers are updated at runtime in lockstep.
14    Lockstep { version: semver::Version },
15
16    /// There are multiple supported versions of this API
17    ///
18    /// Clients and servers may be updated independently of each other.  Other
19    /// parts of the system may constrain things so that either clients or
20    /// servers are always updated first, but this tool does not assume that.
21    Versioned { supported_versions: SupportedVersions },
22}
23
24impl Versions {
25    /// Constructor for a lockstep API
26    pub fn new_lockstep(version: semver::Version) -> Versions {
27        Versions::Lockstep { version }
28    }
29
30    /// Constructor for a versioned API
31    pub fn new_versioned(supported_versions: SupportedVersions) -> Versions {
32        Versions::Versioned { supported_versions }
33    }
34
35    /// Returns whether this API is versioned (as opposed to lockstep)
36    pub fn is_versioned(&self) -> bool {
37        match self {
38            Versions::Lockstep { .. } => false,
39            Versions::Versioned { .. } => true,
40        }
41    }
42
43    /// Returns whether this API is lockstep (as opposed to versioned)
44    pub fn is_lockstep(&self) -> bool {
45        match self {
46            Versions::Lockstep { .. } => true,
47            Versions::Versioned { .. } => false,
48        }
49    }
50
51    /// Iterate over the semver versions of an API that are supported
52    pub fn iter_versions_semvers(&self) -> IterVersionsSemvers<'_> {
53        match self {
54            Versions::Lockstep { version } => IterVersionsSemvers {
55                inner: IterVersionsSemversInner::Lockstep(Some(version)),
56            },
57            Versions::Versioned { supported_versions } => IterVersionsSemvers {
58                inner: IterVersionsSemversInner::Versioned(
59                    supported_versions.versions.iter(),
60                ),
61            },
62        }
63    }
64
65    /// For versioned APIs only, iterate over the SupportedVersions
66    pub fn iter_versioned_versions(
67        &self,
68    ) -> Option<impl Iterator<Item = &SupportedVersion> + '_> {
69        match self {
70            Versions::Lockstep { .. } => None,
71            Versions::Versioned { supported_versions } => {
72                Some(supported_versions.iter())
73            }
74        }
75    }
76}
77
78#[derive(Clone, Debug)]
79pub struct SupportedVersion {
80    semver: semver::Version,
81    label: &'static str,
82}
83
84impl SupportedVersion {
85    pub const fn new(
86        semver: semver::Version,
87        label: &'static str,
88    ) -> SupportedVersion {
89        SupportedVersion { semver, label }
90    }
91
92    pub fn semver(&self) -> &semver::Version {
93        &self.semver
94    }
95
96    pub fn label(&self) -> &str {
97        self.label
98    }
99}
100
101#[derive(Clone, Debug)]
102pub struct SupportedVersions {
103    versions: Vec<SupportedVersion>,
104}
105
106impl SupportedVersions {
107    #[track_caller]
108    pub fn new(versions: Vec<SupportedVersion>) -> SupportedVersions {
109        assert!(
110            !versions.is_empty(),
111            "at least one version of an API must be supported"
112        );
113
114        // We require that the list of supported versions for an API be sorted
115        // because this helps ensure a git conflict when two people attempt to
116        // add or modify the same version in different branches.
117        assert!(
118            versions.iter().map(|v| v.semver()).is_sorted(),
119            "list of supported versions for an API must be sorted"
120        );
121
122        // Each semver and each label must be unique.
123        let mut unique_versions = BTreeMap::new();
124        let mut unique_labels = BTreeMap::new();
125        for v in &versions {
126            if let Some(previous) =
127                unique_versions.insert(v.semver(), v.label())
128            {
129                panic!(
130                    "version {} appears multiple times (labels: {:?}, {:?})",
131                    v.semver(),
132                    previous,
133                    v.label()
134                );
135            }
136
137            if let Some(previous) = unique_labels.insert(v.label(), v.semver())
138            {
139                panic!(
140                    "label {:?} appears multiple times (versions: {}, {})",
141                    v.label(),
142                    previous,
143                    v.semver()
144                );
145            }
146        }
147
148        SupportedVersions { versions }
149    }
150
151    pub fn iter(&self) -> impl Iterator<Item = &'_ SupportedVersion> + '_ {
152        self.versions.iter()
153    }
154}
155
156#[derive(Debug)]
157pub struct IterVersionsSemvers<'a> {
158    inner: IterVersionsSemversInner<'a>,
159}
160
161impl<'a> Iterator for IterVersionsSemvers<'a> {
162    type Item = &'a semver::Version;
163
164    fn next(&mut self) -> Option<Self::Item> {
165        self.inner.next()
166    }
167}
168
169impl<'a> ExactSizeIterator for IterVersionsSemvers<'a> {
170    fn len(&self) -> usize {
171        self.inner.len()
172    }
173}
174
175impl<'a> DoubleEndedIterator for IterVersionsSemvers<'a> {
176    fn next_back(&mut self) -> Option<Self::Item> {
177        self.inner.next_back()
178    }
179}
180
181#[derive(Debug)]
182enum IterVersionsSemversInner<'a> {
183    Lockstep(Option<&'a semver::Version>),
184    Versioned(std::slice::Iter<'a, SupportedVersion>),
185}
186
187impl<'a> Iterator for IterVersionsSemversInner<'a> {
188    type Item = &'a semver::Version;
189
190    fn next(&mut self) -> Option<Self::Item> {
191        match self {
192            IterVersionsSemversInner::Lockstep(version) => version.take(),
193            IterVersionsSemversInner::Versioned(versions) => {
194                versions.next().map(|v| &v.semver)
195            }
196        }
197    }
198
199    fn size_hint(&self) -> (usize, Option<usize>) {
200        (self.len(), Some(self.len()))
201    }
202}
203
204impl<'a> ExactSizeIterator for IterVersionsSemversInner<'a> {
205    fn len(&self) -> usize {
206        match self {
207            IterVersionsSemversInner::Lockstep(version) => {
208                usize::from(version.is_some())
209            }
210            IterVersionsSemversInner::Versioned(versions) => versions.len(),
211        }
212    }
213}
214
215impl<'a> DoubleEndedIterator for IterVersionsSemversInner<'a> {
216    fn next_back(&mut self) -> Option<Self::Item> {
217        match self {
218            IterVersionsSemversInner::Lockstep(version) => version.take(),
219            IterVersionsSemversInner::Versioned(versions) => {
220                versions.next_back().map(|v| &v.semver)
221            }
222        }
223    }
224}
225
226/// Helper macro used to define API versions.
227///
228/// ```
229/// use dropshot_api_manager_types::{
230///     SupportedVersion, SupportedVersions, api_versions,
231/// };
232///
233/// api_versions!([
234///     // Define the API versions here. They must be in descending order.
235///     (2, ADD_FOOBAR_OPERATION),
236///     (1, INITIAL),
237/// ]);
238/// ```
239///
240/// This example says that there are two API versions: `1.0.0` (the initial
241/// version) and `2.0.0` (which adds an operation called "foobar").  This macro
242/// invocation defines symbolic constants of type `semver::Version` for each of
243/// these, equivalent to:
244///
245/// ```
246/// pub const VERSION_ADD_FOOBAR_OPERATION: semver::Version =
247///     semver::Version::new(2, 0, 0);
248/// pub const VERSION_INITIAL: semver::Version = semver::Version::new(1, 0, 0);
249/// ```
250///
251/// It also defines two functions:
252///
253/// * `pub fn supported_versions() -> SupportedVersions` that,
254///   as the name suggests, returns a [`SupportedVersions`] that describes these
255///   two supported API versions.
256///
257/// * `pub fn latest_version() -> semver::Version` that returns the latest
258///   supported API version. The latest supported version is the first version
259///   in the list (hence versions must be in descending order).
260// Design constraints:
261// - For each new API version, we need a developer-chosen semver and label that
262//   can be used to construct an identifier.
263// - We want to produce:
264//   - a symbolic constant for each version that won't change if the developer
265//     needs to change the semver value for this API version
266//   - a list of supported API versions
267// - Critically, we want to ensure that if two developers both add new API
268//   versions in separate branches, whether or not they choose the same value,
269//   there must be a git conflict that requires manual resolution.
270//   - To achieve this, we put the list of versions in a list.
271// - We want to make it hard to do this merge wrong without noticing.
272//   - We want to require that the list be sorted (so that someone hasn't put
273//     something in the wrong order).
274//   - The list should have no duplicates.
275// - We want to minimize boilerplate.
276//
277// That's how we've landed on defining API versions using this macro where:
278// - each API definition is simple and fits on a single line
279// - there will necessarily be a conflict if two people try to add a line in the
280//   same spot of the file, even if they overlap, assuming they choose different
281//   labels for their API version
282// - the consumer of this value will be able to do those checks that help make
283//   sure there wasn't a mismerge.
284#[macro_export]
285macro_rules! api_versions {
286    (
287        [
288            (
289                $latest_major:literal,
290                $latest_name: ident
291            )
292            $(,
293                (
294                    $major:literal,
295                    $name:ident
296                )
297            )*
298            $(,)?
299        ] ) => {
300        dropshot_api_manager_types::paste! {
301            pub const [<VERSION_ $latest_name>]: $crate::semver::Version =
302                $crate::semver::Version::new($latest_major, 0, 0);
303
304            $(
305                pub const [<VERSION_ $name>]: $crate::semver::Version =
306                    $crate::semver::Version::new($major, 0, 0);
307            )*
308
309            pub fn supported_versions() -> $crate::SupportedVersions {
310                let mut literal_versions = vec![
311                    $crate::SupportedVersion::new([<VERSION_ $latest_name>], stringify!($latest_name)),
312                    $( $crate::SupportedVersion::new([<VERSION_ $name>], stringify!($name)) ),*
313                ];
314                literal_versions.reverse();
315                $crate::SupportedVersions::new(literal_versions)
316            }
317
318            pub const fn latest_version() -> $crate::semver::Version {
319                [<VERSION_ $latest_name>]
320            }
321        }
322    };
323}
324
325/// "picky" version of `api_versions` that lets you specify the minor and patch
326/// numbers, too
327///
328/// It is not yet clear why we'd ever need this.  Our approach to versioning is
329/// oriented around not having to care whether a change is a major bump or not
330/// so we can just always bump the major number.
331#[macro_export]
332macro_rules! api_versions_picky {
333    ( [
334        (
335            $latest_major:literal,
336            $latest_minor:literal,
337            $latest_patch:literal,
338            $latest_name: ident
339        )
340        $(,
341            (
342                $major:literal,
343                $minor:literal,
344                $patch:literal,
345                $name:ident
346            )
347        )* $(,)? ] ) => {
348        dropshot_api_manager_types::paste! {
349            pub const [<VERSION_ $latest_name>]: $crate::semver::Version =
350                $crate::semver::Version::new($latest_major, $latest_minor, $latest_patch);
351
352            $(
353                pub const [<VERSION_ $name>]: $crate::semver::Version =
354                    $crate::semver::Version::new($major, $minor, $patch);
355            )*
356
357            pub fn supported_versions() -> $crate::SupportedVersions {
358                let mut literal_versions = vec![
359                    $crate::SupportedVersion::new([<VERSION_ $latest_name>], stringify!($latest_name)),
360                    $( $crate::SupportedVersion::new([<VERSION_ $name>], stringify!($name)) ),*
361                ];
362                literal_versions.reverse();
363                $crate::SupportedVersions::new(literal_versions)
364            }
365
366            pub const fn latest_version() -> $crate::semver::Version {
367                [<VERSION_ $latest_name>]
368            }
369        }
370    };
371}