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
169#[derive(Debug)]
170enum IterVersionsSemversInner<'a> {
171 Lockstep(Option<&'a semver::Version>),
172 Versioned(std::slice::Iter<'a, SupportedVersion>),
173}
174
175impl<'a> Iterator for IterVersionsSemversInner<'a> {
176 type Item = &'a semver::Version;
177
178 fn next(&mut self) -> Option<Self::Item> {
179 match self {
180 IterVersionsSemversInner::Lockstep(version) => version.take(),
181 IterVersionsSemversInner::Versioned(versions) => {
182 versions.next().map(|v| &v.semver)
183 }
184 }
185 }
186
187 fn size_hint(&self) -> (usize, Option<usize>) {
188 (self.len(), Some(self.len()))
189 }
190}
191
192impl<'a> ExactSizeIterator for IterVersionsSemversInner<'a> {
193 fn len(&self) -> usize {
194 match self {
195 IterVersionsSemversInner::Lockstep(version) => {
196 usize::from(version.is_some())
197 }
198 IterVersionsSemversInner::Versioned(versions) => versions.len(),
199 }
200 }
201}
202
203/// Helper macro used to define API versions
204///
205/// ```
206/// use dropshot_api_manager_types::{
207/// SupportedVersion, SupportedVersions, api_versions,
208/// };
209///
210/// api_versions!([
211/// // Define the API versions here.
212/// (2, ADD_FOOBAR_OPERATION),
213/// (1, INITIAL),
214/// ]);
215/// ```
216///
217/// This example says that there are two API versions: `1.0.0` (the initial
218/// version) and `2.0.0` (which adds an operation called "foobar"). This macro
219/// invocation defines symbolic constants of type `semver::Version` for each of
220/// these, equivalent to:
221///
222/// ```
223/// pub const VERSION_ADD_FOOBAR_OPERATION: semver::Version =
224/// semver::Version::new(2, 0, 0);
225/// pub const VERSION_INITIAL: semver::Version = semver::Version::new(1, 0, 0);
226/// ```
227///
228/// It also defines a function called `pub fn supported_versions() ->
229/// SupportedVersions` that, as the name suggests, returns a
230/// [`SupportedVersions`] that describes these two supported API versions.
231// Design constraints:
232// - For each new API version, we need a developer-chosen semver and label that
233// can be used to construct an identifier.
234// - We want to produce:
235// - a symbolic constant for each version that won't change if the developer
236// needs to change the semver value for this API version
237// - a list of supported API versions
238// - Critically, we want to ensure that if two developers both add new API
239// versions in separate branches, whether or not they choose the same value,
240// there must be a git conflict that requires manual resolution.
241// - To achieve this, we put the list of versions in a list.
242// - We want to make it hard to do this merge wrong without noticing.
243// - We want to require that the list be sorted (so that someone hasn't put
244// something in the wrong order).
245// - The list should have no duplicates.
246// - We want to minimize boilerplate.
247//
248// That's how we've landed on defining API versions using this macro where:
249// - each API definition is simple and fits on a single line
250// - there will necessarily be a conflict if two people try to add a line in the
251// same spot of the file, even if they overlap, assuming they choose different
252// labels for their API version
253// - the consumer of this value will be able to do those checks that help make
254// sure there wasn't a mismerge.
255#[macro_export]
256macro_rules! api_versions {
257 ( [ $( (
258 $major:literal,
259 $name:ident
260 ) ),* $(,)? ] ) => {
261 dropshot_api_manager_types::paste! {
262 $(
263 pub const [<VERSION_ $name>]: $crate::semver::Version =
264 $crate::semver::Version::new($major, 0, 0);
265 )*
266
267 pub fn supported_versions() -> $crate::SupportedVersions {
268 let mut literal_versions = vec![
269 $( $crate::SupportedVersion::new([<VERSION_ $name>], stringify!($name)) ),*
270 ];
271 literal_versions.reverse();
272 $crate::SupportedVersions::new(literal_versions)
273 }
274 }
275 };
276}
277
278/// "picky" version of `api_versions` that lets you specify the minor and patch
279/// numbers, too
280///
281/// It is not yet clear why we'd ever need this. Our approach to versioning is
282/// oriented around not having to care whether a change is a major bump or not
283/// so we can just always bump the major number.
284#[macro_export]
285macro_rules! api_versions_picky {
286 ( [ $( (
287 $major:literal,
288 $minor:literal,
289 $patch:literal,
290 $name:ident
291 ) ),* $(,)? ] ) => {
292 dropshot_api_manager_types::paste! {
293 $(
294 pub const [<VERSION_ $name>]: semver::Version =
295 semver::Version::new($major, $minor, $patch);
296 )*
297
298 #[track_caller]
299 pub fn supported_versions() -> SupportedVersions {
300 let mut literal_versions = vec![
301 $( SupportedVersion::new([<VERSION_ $name>], $desc) ),*
302 ];
303 literal_versions.reverse();
304 SupportedVersions::new(literal_versions)
305 }
306 }
307 };
308}