Skip to content

Commit 9f774d1

Browse files
authored
feat(stackable-versioned): Use enum for merged_crd instead of str (#872)
* feat: Use enum for merged_crd instead of str This ensures that the user must use a valid version to specify which of the versions of the merged CRDs is stored. Previously, users could provide any str, which can lead to runtime errors. * chore: Update changelog
1 parent 7fe86d5 commit 9f774d1

File tree

4 files changed

+127
-45
lines changed

4 files changed

+127
-45
lines changed

crates/stackable-versioned-macros/src/codegen/common/container.rs

+12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::ops::Deref;
22

3+
use convert_case::{Case, Casing};
4+
use k8s_version::Version;
35
use proc_macro2::TokenStream;
46
use quote::format_ident;
57
use syn::{Attribute, Ident, Visibility};
@@ -52,6 +54,16 @@ impl IdentExt for Ident {
5254
}
5355
}
5456

57+
pub(crate) trait VersionExt {
58+
fn as_variant_ident(&self) -> Ident;
59+
}
60+
61+
impl VersionExt for Version {
62+
fn as_variant_ident(&self) -> Ident {
63+
format_ident!("{ident}", ident = self.to_string().to_case(Case::Pascal))
64+
}
65+
}
66+
5567
/// This struct bundles values from [`DeriveInput`][1].
5668
///
5769
/// [`DeriveInput`][1] cannot be used directly when constructing a

crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs

+102-44
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ use syn::{parse_quote, DataStruct, Error, Ident};
88
use crate::{
99
attrs::common::ContainerAttributes,
1010
codegen::{
11-
common::{Container, ContainerInput, ContainerVersion, Item, VersionedContainer},
11+
common::{
12+
Container, ContainerInput, ContainerVersion, Item, KubernetesOptions, VersionExt,
13+
VersionedContainer,
14+
},
1215
vstruct::field::VersionedField,
1316
},
1417
};
1518

1619
pub(crate) mod field;
1720

21+
type GenerateVersionReturn = (TokenStream, Option<(TokenStream, (Ident, String))>);
22+
1823
/// Stores individual versions of a single struct. Each version tracks field
1924
/// actions, which describe if the field was added, renamed or deprecated in
2025
/// that version. Fields which are not versioned, are included in every
@@ -85,24 +90,30 @@ impl Container<DataStruct, VersionedField> for VersionedStruct {
8590
}
8691

8792
fn generate_tokens(&self) -> TokenStream {
88-
let mut kubernetes_crd_fn_calls = TokenStream::new();
89-
let mut container_definition = TokenStream::new();
93+
let mut tokens = TokenStream::new();
94+
95+
let mut enum_variants = Vec::new();
96+
let mut crd_fn_calls = Vec::new();
9097

9198
let mut versions = self.versions.iter().peekable();
9299

93100
while let Some(version) = versions.next() {
94-
container_definition.extend(self.generate_version(version, versions.peek().copied()));
95-
kubernetes_crd_fn_calls.extend(self.generate_kubernetes_crd_fn_call(version));
101+
let (container_definition, merged_crd) =
102+
self.generate_version(version, versions.peek().copied());
103+
104+
if let Some((crd_fn_call, enum_variant)) = merged_crd {
105+
enum_variants.push(enum_variant);
106+
crd_fn_calls.push(crd_fn_call);
107+
}
108+
109+
tokens.extend(container_definition);
96110
}
97111

98-
// If tokens for the 'crd()' function calls were generated, also generate
99-
// the 'merge_crds' call.
100-
if !kubernetes_crd_fn_calls.is_empty() {
101-
container_definition
102-
.extend(self.generate_kubernetes_merge_crds(kubernetes_crd_fn_calls));
112+
if !crd_fn_calls.is_empty() {
113+
tokens.extend(self.generate_kubernetes_merge_crds(crd_fn_calls, enum_variants));
103114
}
104115

105-
container_definition
116+
tokens
106117
}
107118
}
108119

@@ -112,7 +123,7 @@ impl VersionedStruct {
112123
&self,
113124
version: &ContainerVersion,
114125
next_version: Option<&ContainerVersion>,
115-
) -> TokenStream {
126+
) -> GenerateVersionReturn {
116127
let mut token_stream = TokenStream::new();
117128

118129
let original_attributes = &self.original_attributes;
@@ -137,7 +148,27 @@ impl VersionedStruct {
137148
let version_specific_docs = self.generate_struct_docs(version);
138149

139150
// Generate K8s specific code
140-
let kubernetes_cr_derive = self.generate_kubernetes_cr_derive(version);
151+
let (kubernetes_cr_derive, merged_crd) = match &self.options.kubernetes_options {
152+
Some(options) => {
153+
// Generate the CustomResource derive macro with the appropriate
154+
// attributes supplied using #[kube()].
155+
let cr_derive = self.generate_kubernetes_cr_derive(version, options);
156+
157+
// Generate merged_crd specific code when not opted out.
158+
let merged_crd = if !options.skip_merged_crd {
159+
let crd_fn_call = self.generate_kubernetes_crd_fn_call(version);
160+
let enum_variant = version.inner.as_variant_ident();
161+
let enum_display = version.inner.to_string();
162+
163+
Some((crd_fn_call, (enum_variant, enum_display)))
164+
} else {
165+
None
166+
};
167+
168+
(Some(cr_derive), merged_crd)
169+
}
170+
None => (None, None),
171+
};
141172

142173
// Generate tokens for the module and the contained struct
143174
token_stream.extend(quote! {
@@ -160,7 +191,7 @@ impl VersionedStruct {
160191
token_stream.extend(self.generate_from_impl(version, next_version));
161192
}
162193

163-
token_stream
194+
(token_stream, merged_crd)
164195
}
165196

166197
/// Generates version specific doc comments for the struct.
@@ -251,63 +282,90 @@ impl VersionedStruct {
251282
impl VersionedStruct {
252283
/// Generates the `kube::CustomResource` derive with the appropriate macro
253284
/// attributes.
254-
fn generate_kubernetes_cr_derive(&self, version: &ContainerVersion) -> Option<TokenStream> {
255-
if let Some(kubernetes_options) = &self.options.kubernetes_options {
256-
let group = &kubernetes_options.group;
257-
let version = version.inner.to_string();
258-
let kind = kubernetes_options
259-
.kind
260-
.as_ref()
261-
.map_or(self.idents.kubernetes.to_string(), |kind| kind.clone());
285+
fn generate_kubernetes_cr_derive(
286+
&self,
287+
version: &ContainerVersion,
288+
options: &KubernetesOptions,
289+
) -> TokenStream {
290+
let group = &options.group;
291+
let version = version.inner.to_string();
292+
let kind = options
293+
.kind
294+
.as_ref()
295+
.map_or(self.idents.kubernetes.to_string(), |kind| kind.clone());
262296

263-
return Some(quote! {
264-
#[derive(::kube::CustomResource)]
265-
#[kube(group = #group, version = #version, kind = #kind)]
266-
});
297+
quote! {
298+
#[derive(::kube::CustomResource)]
299+
#[kube(group = #group, version = #version, kind = #kind)]
267300
}
268-
269-
None
270301
}
271302

272303
/// Generates the `merge_crds` function call.
273-
fn generate_kubernetes_merge_crds(&self, fn_calls: TokenStream) -> TokenStream {
304+
fn generate_kubernetes_merge_crds(
305+
&self,
306+
crd_fn_calls: Vec<TokenStream>,
307+
enum_variants: Vec<(Ident, String)>,
308+
) -> TokenStream {
274309
let ident = &self.idents.kubernetes;
275310

311+
let version_enum_definition = self.generate_kubernetes_version_enum(enum_variants);
312+
276313
quote! {
277314
#[automatically_derived]
278315
pub struct #ident;
279316

317+
#version_enum_definition
318+
280319
#[automatically_derived]
281320
impl #ident {
282321
/// Generates a merged CRD which contains all versions defined using the
283322
/// `#[versioned()]` macro.
284323
pub fn merged_crd(
285-
stored_apiversion: &str
324+
stored_apiversion: Version
286325
) -> ::std::result::Result<::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError> {
287-
::kube::core::crd::merge_crds(vec![#fn_calls], stored_apiversion)
326+
::kube::core::crd::merge_crds(vec![#(#crd_fn_calls),*], &stored_apiversion.to_string())
288327
}
289328
}
290329
}
291330
}
292331

293332
/// Generates the inner `crd()` functions calls which get used in the
294333
/// `merge_crds` function.
295-
fn generate_kubernetes_crd_fn_call(&self, version: &ContainerVersion) -> Option<TokenStream> {
296-
if self
297-
.options
298-
.kubernetes_options
299-
.as_ref()
300-
.is_some_and(|o| !o.skip_merged_crd)
301-
{
302-
let struct_ident = &self.idents.kubernetes;
303-
let version_ident = &version.ident;
334+
fn generate_kubernetes_crd_fn_call(&self, version: &ContainerVersion) -> TokenStream {
335+
let struct_ident = &self.idents.kubernetes;
336+
let version_ident = &version.ident;
337+
let path: syn::Path = parse_quote!(#version_ident::#struct_ident);
304338

305-
let path: syn::Path = parse_quote!(#version_ident::#struct_ident);
306-
return Some(quote! {
307-
<#path as ::kube::CustomResourceExt>::crd(),
339+
quote! {
340+
<#path as ::kube::CustomResourceExt>::crd()
341+
}
342+
}
343+
344+
fn generate_kubernetes_version_enum(&self, enum_variants: Vec<(Ident, String)>) -> TokenStream {
345+
let mut enum_variant_matches = TokenStream::new();
346+
let mut enum_variant_idents = TokenStream::new();
347+
348+
for (enum_variant_ident, enum_variant_display) in enum_variants {
349+
enum_variant_idents.extend(quote! {#enum_variant_ident,});
350+
enum_variant_matches.extend(quote! {
351+
Version::#enum_variant_ident => f.write_str(#enum_variant_display),
308352
});
309353
}
310354

311-
None
355+
quote! {
356+
#[automatically_derived]
357+
pub enum Version {
358+
#enum_variant_idents
359+
}
360+
361+
#[automatically_derived]
362+
impl ::std::fmt::Display for Version {
363+
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> {
364+
match self {
365+
#enum_variant_matches
366+
}
367+
}
368+
}
369+
}
312370
}
313371
}

crates/stackable-versioned-macros/tests/k8s/pass/crd.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@ fn main() {
2121
baz: bool,
2222
}
2323

24-
let merged_crd = Foo::merged_crd("v1").unwrap();
24+
let merged_crd = Foo::merged_crd(Version::V1).unwrap();
2525
println!("{}", serde_yaml::to_string(&merged_crd).unwrap());
2626
}

crates/stackable-versioned/CHANGELOG.md

+12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- Generate a `Version` enum containing all declared versions as variants
10+
([#872]).
11+
12+
### Changed
13+
14+
- The `merged_crd` associated function now takes `Version` instead of `&str` as
15+
input ([#872]).
16+
17+
[#872]: https://github.com/stackabletech/operator-rs/pull/872
18+
719
## [0.2.0] - 2024-09-19
820

921
### Added

0 commit comments

Comments
 (0)