diff --git a/crates/stackable-versioned-macros/src/codegen/common/container.rs b/crates/stackable-versioned-macros/src/codegen/common/container.rs index 26011259b..c4d427d6e 100644 --- a/crates/stackable-versioned-macros/src/codegen/common/container.rs +++ b/crates/stackable-versioned-macros/src/codegen/common/container.rs @@ -1,5 +1,7 @@ use std::ops::Deref; +use convert_case::{Case, Casing}; +use k8s_version::Version; use proc_macro2::TokenStream; use quote::format_ident; use syn::{Attribute, Ident, Visibility}; @@ -52,6 +54,16 @@ impl IdentExt for Ident { } } +pub(crate) trait VersionExt { + fn as_variant_ident(&self) -> Ident; +} + +impl VersionExt for Version { + fn as_variant_ident(&self) -> Ident { + format_ident!("{ident}", ident = self.to_string().to_case(Case::Pascal)) + } +} + /// This struct bundles values from [`DeriveInput`][1]. /// /// [`DeriveInput`][1] cannot be used directly when constructing a diff --git a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs b/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs index 6dd493906..2ed889f69 100644 --- a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs @@ -8,13 +8,18 @@ use syn::{parse_quote, DataStruct, Error, Ident}; use crate::{ attrs::common::ContainerAttributes, codegen::{ - common::{Container, ContainerInput, ContainerVersion, Item, VersionedContainer}, + common::{ + Container, ContainerInput, ContainerVersion, Item, KubernetesOptions, VersionExt, + VersionedContainer, + }, vstruct::field::VersionedField, }, }; pub(crate) mod field; +type GenerateVersionReturn = (TokenStream, Option<(TokenStream, (Ident, String))>); + /// Stores individual versions of a single struct. Each version tracks field /// actions, which describe if the field was added, renamed or deprecated in /// that version. Fields which are not versioned, are included in every @@ -85,24 +90,30 @@ impl Container for VersionedStruct { } fn generate_tokens(&self) -> TokenStream { - let mut kubernetes_crd_fn_calls = TokenStream::new(); - let mut container_definition = TokenStream::new(); + let mut tokens = TokenStream::new(); + + let mut enum_variants = Vec::new(); + let mut crd_fn_calls = Vec::new(); let mut versions = self.versions.iter().peekable(); while let Some(version) = versions.next() { - container_definition.extend(self.generate_version(version, versions.peek().copied())); - kubernetes_crd_fn_calls.extend(self.generate_kubernetes_crd_fn_call(version)); + let (container_definition, merged_crd) = + self.generate_version(version, versions.peek().copied()); + + if let Some((crd_fn_call, enum_variant)) = merged_crd { + enum_variants.push(enum_variant); + crd_fn_calls.push(crd_fn_call); + } + + tokens.extend(container_definition); } - // If tokens for the 'crd()' function calls were generated, also generate - // the 'merge_crds' call. - if !kubernetes_crd_fn_calls.is_empty() { - container_definition - .extend(self.generate_kubernetes_merge_crds(kubernetes_crd_fn_calls)); + if !crd_fn_calls.is_empty() { + tokens.extend(self.generate_kubernetes_merge_crds(crd_fn_calls, enum_variants)); } - container_definition + tokens } } @@ -112,7 +123,7 @@ impl VersionedStruct { &self, version: &ContainerVersion, next_version: Option<&ContainerVersion>, - ) -> TokenStream { + ) -> GenerateVersionReturn { let mut token_stream = TokenStream::new(); let original_attributes = &self.original_attributes; @@ -137,7 +148,27 @@ impl VersionedStruct { let version_specific_docs = self.generate_struct_docs(version); // Generate K8s specific code - let kubernetes_cr_derive = self.generate_kubernetes_cr_derive(version); + let (kubernetes_cr_derive, merged_crd) = match &self.options.kubernetes_options { + Some(options) => { + // Generate the CustomResource derive macro with the appropriate + // attributes supplied using #[kube()]. + let cr_derive = self.generate_kubernetes_cr_derive(version, options); + + // Generate merged_crd specific code when not opted out. + let merged_crd = if !options.skip_merged_crd { + let crd_fn_call = self.generate_kubernetes_crd_fn_call(version); + let enum_variant = version.inner.as_variant_ident(); + let enum_display = version.inner.to_string(); + + Some((crd_fn_call, (enum_variant, enum_display))) + } else { + None + }; + + (Some(cr_derive), merged_crd) + } + None => (None, None), + }; // Generate tokens for the module and the contained struct token_stream.extend(quote! { @@ -160,7 +191,7 @@ impl VersionedStruct { token_stream.extend(self.generate_from_impl(version, next_version)); } - token_stream + (token_stream, merged_crd) } /// Generates version specific doc comments for the struct. @@ -251,40 +282,48 @@ impl VersionedStruct { impl VersionedStruct { /// Generates the `kube::CustomResource` derive with the appropriate macro /// attributes. - fn generate_kubernetes_cr_derive(&self, version: &ContainerVersion) -> Option { - if let Some(kubernetes_options) = &self.options.kubernetes_options { - let group = &kubernetes_options.group; - let version = version.inner.to_string(); - let kind = kubernetes_options - .kind - .as_ref() - .map_or(self.idents.kubernetes.to_string(), |kind| kind.clone()); + fn generate_kubernetes_cr_derive( + &self, + version: &ContainerVersion, + options: &KubernetesOptions, + ) -> TokenStream { + let group = &options.group; + let version = version.inner.to_string(); + let kind = options + .kind + .as_ref() + .map_or(self.idents.kubernetes.to_string(), |kind| kind.clone()); - return Some(quote! { - #[derive(::kube::CustomResource)] - #[kube(group = #group, version = #version, kind = #kind)] - }); + quote! { + #[derive(::kube::CustomResource)] + #[kube(group = #group, version = #version, kind = #kind)] } - - None } /// Generates the `merge_crds` function call. - fn generate_kubernetes_merge_crds(&self, fn_calls: TokenStream) -> TokenStream { + fn generate_kubernetes_merge_crds( + &self, + crd_fn_calls: Vec, + enum_variants: Vec<(Ident, String)>, + ) -> TokenStream { let ident = &self.idents.kubernetes; + let version_enum_definition = self.generate_kubernetes_version_enum(enum_variants); + quote! { #[automatically_derived] pub struct #ident; + #version_enum_definition + #[automatically_derived] impl #ident { /// Generates a merged CRD which contains all versions defined using the /// `#[versioned()]` macro. pub fn merged_crd( - stored_apiversion: &str + stored_apiversion: Version ) -> ::std::result::Result<::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError> { - ::kube::core::crd::merge_crds(vec![#fn_calls], stored_apiversion) + ::kube::core::crd::merge_crds(vec![#(#crd_fn_calls),*], &stored_apiversion.to_string()) } } } @@ -292,22 +331,41 @@ impl VersionedStruct { /// Generates the inner `crd()` functions calls which get used in the /// `merge_crds` function. - fn generate_kubernetes_crd_fn_call(&self, version: &ContainerVersion) -> Option { - if self - .options - .kubernetes_options - .as_ref() - .is_some_and(|o| !o.skip_merged_crd) - { - let struct_ident = &self.idents.kubernetes; - let version_ident = &version.ident; + fn generate_kubernetes_crd_fn_call(&self, version: &ContainerVersion) -> TokenStream { + let struct_ident = &self.idents.kubernetes; + let version_ident = &version.ident; + let path: syn::Path = parse_quote!(#version_ident::#struct_ident); - let path: syn::Path = parse_quote!(#version_ident::#struct_ident); - return Some(quote! { - <#path as ::kube::CustomResourceExt>::crd(), + quote! { + <#path as ::kube::CustomResourceExt>::crd() + } + } + + fn generate_kubernetes_version_enum(&self, enum_variants: Vec<(Ident, String)>) -> TokenStream { + let mut enum_variant_matches = TokenStream::new(); + let mut enum_variant_idents = TokenStream::new(); + + for (enum_variant_ident, enum_variant_display) in enum_variants { + enum_variant_idents.extend(quote! {#enum_variant_ident,}); + enum_variant_matches.extend(quote! { + Version::#enum_variant_ident => f.write_str(#enum_variant_display), }); } - None + quote! { + #[automatically_derived] + pub enum Version { + #enum_variant_idents + } + + #[automatically_derived] + impl ::std::fmt::Display for Version { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { + match self { + #enum_variant_matches + } + } + } + } } } diff --git a/crates/stackable-versioned-macros/tests/k8s/pass/crd.rs b/crates/stackable-versioned-macros/tests/k8s/pass/crd.rs index 0defd8252..c9255d8fa 100644 --- a/crates/stackable-versioned-macros/tests/k8s/pass/crd.rs +++ b/crates/stackable-versioned-macros/tests/k8s/pass/crd.rs @@ -21,6 +21,6 @@ fn main() { baz: bool, } - let merged_crd = Foo::merged_crd("v1").unwrap(); + let merged_crd = Foo::merged_crd(Version::V1).unwrap(); println!("{}", serde_yaml::to_string(&merged_crd).unwrap()); } diff --git a/crates/stackable-versioned/CHANGELOG.md b/crates/stackable-versioned/CHANGELOG.md index 053abf133..a66ea0c86 100644 --- a/crates/stackable-versioned/CHANGELOG.md +++ b/crates/stackable-versioned/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Generate a `Version` enum containing all declared versions as variants + ([#872]). + +### Changed + +- The `merged_crd` associated function now takes `Version` instead of `&str` as + input ([#872]). + +[#872]: https://github.com/stackabletech/operator-rs/pull/872 + ## [0.2.0] - 2024-09-19 ### Added