Skip to main content

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