diff --git a/Cargo.lock b/Cargo.lock index 0a97a055a..8f2e8d90d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,7 +131,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -142,7 +142,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -353,7 +353,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.74", + "syn 2.0.77", "which", ] @@ -489,7 +489,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -649,7 +649,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -660,7 +660,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -671,7 +671,7 @@ checksum = "4e018fccbeeb50ff26562ece792ed06659b9c2dae79ece77c4456bb10d9bf79b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -695,7 +695,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -986,7 +986,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -1548,7 +1548,7 @@ dependencies = [ "rstest", "rstest_reuse", "snafu 0.8.4", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -1629,7 +1629,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -2112,7 +2112,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -2143,7 +2143,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -2201,7 +2201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -2267,7 +2267,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -2440,7 +2440,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.74", + "syn 2.0.77", "unicode-ident", ] @@ -2452,7 +2452,7 @@ checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14" dependencies = [ "quote", "rand", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -2589,7 +2589,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -2678,7 +2678,7 @@ checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -2689,7 +2689,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -2858,7 +2858,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -2956,7 +2956,7 @@ dependencies = [ "proc-macro2", "quote", "stackable-operator", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -2994,12 +2994,18 @@ dependencies = [ "convert_case", "darling", "itertools 0.13.0", + "k8s-openapi", "k8s-version", + "kube", "proc-macro2", "quote", "rstest", + "schemars", + "serde", + "serde_json", + "serde_yaml", "strum", - "syn 2.0.74", + "syn 2.0.77", "trybuild", ] @@ -3052,7 +3058,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -3074,9 +3080,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.74" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -3134,7 +3140,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -3233,7 +3239,7 @@ checksum = "8d9ef545650e79f30233c0003bcc2504d7efac6dad25fca40744de773fe2049c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -3271,7 +3277,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -3466,7 +3472,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -3683,7 +3689,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", "wasm-bindgen-shared", ] @@ -3705,7 +3711,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3907,7 +3913,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] [[package]] @@ -3927,5 +3933,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.77", ] diff --git a/Cargo.toml b/Cargo.toml index 85b99aa73..4618b963f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ signature = "2.2.0" snafu = "0.8.4" stackable-operator-derive = { path = "stackable-operator-derive" } strum = { version = "0.26.3", features = ["derive"] } -syn = "2.0.72" +syn = "2.0.77" tempfile = "3.11.0" time = { version = "0.3.36" } tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread", "fs"] } diff --git a/crates/stackable-versioned-macros/Cargo.toml b/crates/stackable-versioned-macros/Cargo.toml index e58943ec7..af29f3d06 100644 --- a/crates/stackable-versioned-macros/Cargo.toml +++ b/crates/stackable-versioned-macros/Cargo.toml @@ -6,15 +6,28 @@ license.workspace = true edition.workspace = true repository.workspace = true +# cargo-udeps throws an error that these dependencies are unused. They are, +# however, used in K8s specific test cases. This is a false-positive and an +# apparent limitation of cargo-udeps. These entries can be removed once +# cargo-udeps supports detecting usage of such dependencies. +[package.metadata.cargo-udeps.ignore] +development = ["schemars", "serde_yaml"] + [lib] proc-macro = true +[features] +full = ["k8s"] +k8s = ["dep:kube", "dep:k8s-openapi"] + [dependencies] k8s-version = { path = "../k8s-version", features = ["darling"] } convert_case.workspace = true darling.workspace = true itertools.workspace = true +k8s-openapi = { workspace = true, optional = true } +kube = { workspace = true, optional = true } proc-macro2.workspace = true strum.workspace = true syn.workspace = true @@ -22,4 +35,8 @@ quote.workspace = true [dev-dependencies] rstest.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_yaml.workspace = true trybuild.workspace = true diff --git a/crates/stackable-versioned-macros/src/attrs/common/container.rs b/crates/stackable-versioned-macros/src/attrs/common/container.rs index 5888d45ca..d5a3ccb0a 100644 --- a/crates/stackable-versioned-macros/src/attrs/common/container.rs +++ b/crates/stackable-versioned-macros/src/attrs/common/container.rs @@ -12,15 +12,19 @@ use k8s_version::Version; /// Currently supported attributes are: /// /// - `version`, which can occur one or more times. See [`VersionAttributes`]. -/// - `options`, which allow further customization of the generated code. See [`ContainerOptions`]. +/// - `options`, which allow further customization of the generated code. +/// See [`ContainerAttributes`]. #[derive(Debug, FromMeta)] #[darling(and_then = ContainerAttributes::validate)] pub(crate) struct ContainerAttributes { #[darling(multiple, rename = "version")] pub(crate) versions: SpannedValue>, - #[darling(default)] - pub(crate) options: ContainerOptions, + #[darling(rename = "k8s")] + pub(crate) kubernetes_attrs: Option, + + #[darling(default, rename = "options")] + pub(crate) common_option_attrs: OptionAttributes, } impl ContainerAttributes { @@ -43,7 +47,7 @@ impl ContainerAttributes { // Ensure that versions are defined in sorted (ascending) order to keep // code consistent. - if !self.options.allow_unsorted.is_present() { + if !self.common_option_attrs.allow_unsorted.is_present() { let original = self.versions.deref().clone(); self.versions .sort_by(|lhs, rhs| lhs.name.partial_cmp(&rhs.name).unwrap_or(Ordering::Equal)); @@ -71,20 +75,28 @@ impl ContainerAttributes { // place. // Ensure every version is unique and isn't declared multiple times. - let duplicates = self + let duplicate_versions = self .versions .iter() .duplicates_by(|e| e.name) .map(|e| e.name) .join(", "); - if !duplicates.is_empty() { + if !duplicate_versions.is_empty() { return Err(Error::custom(format!( - "attribute macro `#[versioned()]` contains duplicate versions: {duplicates}", + "attribute macro `#[versioned()]` contains duplicate versions: {duplicate_versions}", )) .with_span(&self.versions.span())); } + // Ensure that the 'k8s' feature is enabled when the 'k8s()' + // attribute is used. + if self.kubernetes_attrs.is_some() && cfg!(not(feature = "k8s")) { + return Err(Error::custom( + "the `#[versioned(k8s())]` attribute can only be used when the `k8s` feature is enabled", + )); + } + Ok(self) } } @@ -101,29 +113,59 @@ impl ContainerAttributes { pub(crate) struct VersionAttributes { pub(crate) deprecated: Flag, pub(crate) name: Version, - pub(crate) skip: Option, + pub(crate) skip: Option, pub(crate) doc: Option, } -/// This struct contains supported container options. +/// This struct contains supported option attributes. /// -/// Supported options are: +/// Supported attributes are: /// /// - `allow_unsorted`, which allows declaring versions in unsorted order, /// instead of enforcing ascending order. /// - `skip` option to skip generating various pieces of code. #[derive(Clone, Debug, Default, FromMeta)] -pub(crate) struct ContainerOptions { +pub(crate) struct OptionAttributes { pub(crate) allow_unsorted: Flag, - pub(crate) skip: Option, + pub(crate) skip: Option, } -/// This struct contains supported skip options. +/// This struct contains supported Kubernetes attributes. /// -/// Supported options are: +/// Supported attributes are: +/// +/// - `skip`, which controls skipping parts of the generation. +/// - `kind`, which allows overwriting the kind field of the CRD. This defaults +/// to the struct name (without the 'Spec' suffix). +/// - `group`, which sets the CRD group, usually the domain of the company. +#[derive(Clone, Debug, FromMeta)] +pub(crate) struct KubernetesAttributes { + pub(crate) skip: Option, + pub(crate) kind: Option, + pub(crate) group: String, +} + +/// This struct contains supported kubernetes skip attributes. +/// +/// Supported attributes are: +/// +/// - `merged_crd` flag, which skips generating the `crd()` and `merged_crd()` +/// functions are generated. +#[derive(Clone, Debug, FromMeta)] +pub(crate) struct KubernetesSkipAttributes { + /// Whether the `crd()` and `merged_crd()` generation should be skipped for + /// this container. + pub(crate) merged_crd: Flag, +} + +/// This struct contains supported common skip attributes. +/// +/// Supported attributes are: /// /// - `from` flag, which skips generating [`From`] implementations when provided. #[derive(Clone, Debug, Default, FromMeta)] -pub(crate) struct SkipOptions { +pub(crate) struct CommonSkipAttributes { + /// Whether the [`From`] implementation generation should be skipped for all + /// versions of this container. pub(crate) from: Flag, } diff --git a/crates/stackable-versioned-macros/src/codegen/common/container.rs b/crates/stackable-versioned-macros/src/codegen/common/container.rs index 1ab77e7c4..26011259b 100644 --- a/crates/stackable-versioned-macros/src/codegen/common/container.rs +++ b/crates/stackable-versioned-macros/src/codegen/common/container.rs @@ -1,6 +1,7 @@ use std::ops::Deref; use proc_macro2::TokenStream; +use quote::format_ident; use syn::{Attribute, Ident, Visibility}; use crate::{attrs::common::ContainerAttributes, codegen::common::ContainerVersion}; @@ -32,6 +33,25 @@ where fn generate_tokens(&self) -> TokenStream; } +/// Provides extra functionality on top of [`struct@Ident`]s. +pub(crate) trait IdentExt { + /// Removes the 'Spec' suffix from the [`struct@Ident`]. + fn as_cleaned_kubernetes_ident(&self) -> Ident; + + /// Transforms the [`struct@Ident`] into one usable in the [`From`] impl. + fn as_from_impl_ident(&self) -> Ident; +} + +impl IdentExt for Ident { + fn as_cleaned_kubernetes_ident(&self) -> Ident { + format_ident!("{}", self.to_string().trim_end_matches("Spec")) + } + + fn as_from_impl_ident(&self) -> Ident { + format_ident!("__sv_{}", self.to_string().to_lowercase()) + } +} + /// This struct bundles values from [`DeriveInput`][1]. /// /// [`DeriveInput`][1] cannot be used directly when constructing a @@ -58,24 +78,95 @@ pub(crate) struct VersionedContainer { /// definition with appropriate items. pub(crate) versions: Vec, + /// The original attributes that were added to the container. + pub(crate) original_attributes: Vec, + + /// The visibility of the versioned container. Used to forward the + /// visibility during code generation. + pub(crate) visibility: Visibility, + /// List of items defined in the original container. How, and if, an item /// should generate code, is decided by the currently generated version. pub(crate) items: Vec, - /// The ident, or name, of the versioned container. - pub(crate) ident: Ident, + /// Different options which influence code generation. + pub(crate) options: VersionedContainerOptions, - /// The visibility of the versioned container. Used to forward the - /// visibility during code generation. - pub(crate) visibility: Visibility, + /// A collection of container idents used for different purposes. + pub(crate) idents: VersionedContainerIdents, +} - /// The original attributes that were added to the container. - pub(crate) original_attributes: Vec, +impl VersionedContainer { + /// Creates a new versioned Container which contains common data shared + /// across structs and enums. + pub(crate) fn new( + input: ContainerInput, + attributes: ContainerAttributes, + versions: Vec, + items: Vec, + ) -> Self { + let ContainerInput { + original_attributes, + visibility, + ident, + } = input; + + let skip_from = attributes + .common_option_attrs + .skip + .map_or(false, |s| s.from.is_present()); + + let kubernetes_options = attributes.kubernetes_attrs.map(|a| KubernetesOptions { + skip_merged_crd: a.skip.map_or(false, |s| s.merged_crd.is_present()), + group: a.group, + kind: a.kind, + }); - /// The name of the container used in `From` implementations. - pub(crate) from_ident: Ident, + let options = VersionedContainerOptions { + kubernetes_options, + skip_from, + }; - /// Whether the [`From`] implementation generation should be skipped for all - /// versions of this container. + let idents = VersionedContainerIdents { + kubernetes: ident.as_cleaned_kubernetes_ident(), + from: ident.as_from_impl_ident(), + original: ident, + }; + + VersionedContainer { + original_attributes, + visibility, + versions, + options, + idents, + items, + } + } +} + +/// A collection of container idents used for different purposes. +#[derive(Debug)] +pub(crate) struct VersionedContainerIdents { + /// The ident used in the context of Kubernetes specific code. This ident + /// removes the 'Spec' suffix present in the definition container. + pub(crate) kubernetes: Ident, + + /// The original ident, or name, of the versioned container. + pub(crate) original: Ident, + + /// The ident used in the [`From`] impl. + pub(crate) from: Ident, +} + +#[derive(Debug)] +pub(crate) struct VersionedContainerOptions { + pub(crate) kubernetes_options: Option, pub(crate) skip_from: bool, } + +#[derive(Debug)] +pub(crate) struct KubernetesOptions { + pub(crate) skip_merged_crd: bool, + pub(crate) kind: Option, + pub(crate) group: String, +} diff --git a/crates/stackable-versioned-macros/src/codegen/common/item.rs b/crates/stackable-versioned-macros/src/codegen/common/item.rs index 65c0b1f96..f97a14311 100644 --- a/crates/stackable-versioned-macros/src/codegen/common/item.rs +++ b/crates/stackable-versioned-macros/src/codegen/common/item.rs @@ -207,7 +207,6 @@ where let mut actions = BTreeMap::new(); for change in common_attributes.changes.iter().rev() { - dbg!(&ty, &change.since); let from_ident = if let Some(from) = change.from_name.as_deref() { format_ident!("{from}") } else { diff --git a/crates/stackable-versioned-macros/src/codegen/common/mod.rs b/crates/stackable-versioned-macros/src/codegen/common/mod.rs index ad35ae1f0..d46cfd160 100644 --- a/crates/stackable-versioned-macros/src/codegen/common/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/common/mod.rs @@ -70,11 +70,6 @@ impl From<&ContainerAttributes> for Vec { } } -/// Returns the container ident used in [`From`] implementations. -pub(crate) fn format_container_from_ident(ident: &Ident) -> Ident { - format_ident!("__sv_{ident}", ident = ident.to_string().to_lowercase()) -} - /// Removes the deprecated prefix from a field ident. /// /// See [`DEPRECATED_FIELD_PREFIX`]. diff --git a/crates/stackable-versioned-macros/src/codegen/venum/mod.rs b/crates/stackable-versioned-macros/src/codegen/venum/mod.rs index a9aefb0ce..d6b691afb 100644 --- a/crates/stackable-versioned-macros/src/codegen/venum/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/venum/mod.rs @@ -8,10 +8,7 @@ use syn::{DataEnum, Error}; use crate::{ attrs::common::ContainerAttributes, codegen::{ - common::{ - format_container_from_ident, Container, ContainerInput, ContainerVersion, Item, - VersionedContainer, - }, + common::{Container, ContainerInput, ContainerVersion, Item, VersionedContainer}, venum::variant::VersionedVariant, }, }; @@ -39,11 +36,7 @@ impl Container for VersionedEnum { data: DataEnum, attributes: ContainerAttributes, ) -> syn::Result { - let ContainerInput { - original_attributes, - visibility, - ident, - } = input; + let ident = &input.ident; // Convert the raw version attributes into a container version. let versions: Vec<_> = (&attributes).into(); @@ -77,20 +70,9 @@ impl Container for VersionedEnum { } } - let from_ident = format_container_from_ident(&ident); - - Ok(Self(VersionedContainer { - skip_from: attributes - .options - .skip - .map_or(false, |s| s.from.is_present()), - original_attributes, - visibility, - from_ident, - versions, - items, - ident, - })) + Ok(Self(VersionedContainer::new( + input, attributes, versions, items, + ))) } fn generate_tokens(&self) -> TokenStream { @@ -114,8 +96,8 @@ impl VersionedEnum { let mut token_stream = TokenStream::new(); let original_attributes = &self.original_attributes; + let enum_name = &self.idents.original; let visibility = &self.visibility; - let enum_name = &self.ident; // Generate variants of the enum for `version`. let variants = self.generate_enum_variants(version); @@ -131,19 +113,8 @@ impl VersionedEnum { .deprecated .then_some(quote! {#[deprecated = #deprecated_note]}); - let mut version_specific_docs = TokenStream::new(); - for (i, doc) in version.version_specific_docs.iter().enumerate() { - if i == 0 { - // Prepend an empty line to clearly separate the version - // specific docs. - version_specific_docs.extend(quote! { - #[doc = ""] - }) - } - version_specific_docs.extend(quote! { - #[doc = #doc] - }) - } + // Generate doc comments for the container (enum) + let version_specific_docs = self.generate_enum_docs(version); // Generate tokens for the module and the contained enum token_stream.extend(quote! { @@ -152,8 +123,8 @@ impl VersionedEnum { #visibility mod #version_ident { use super::*; - #(#original_attributes)* #version_specific_docs + #(#original_attributes)* pub enum #enum_name { #variants } @@ -161,13 +132,33 @@ impl VersionedEnum { }); // Generate the From impl between this `version` and the next one. - if !self.skip_from && !version.skip_from { + if !self.options.skip_from && !version.skip_from { token_stream.extend(self.generate_from_impl(version, next_version)); } token_stream } + /// Generates version specific doc comments for the enum. + fn generate_enum_docs(&self, version: &ContainerVersion) -> TokenStream { + let mut tokens = TokenStream::new(); + + for (i, doc) in version.version_specific_docs.iter().enumerate() { + if i == 0 { + // Prepend an empty line to clearly separate the version + // specific docs. + tokens.extend(quote! { + #[doc = ""] + }) + } + tokens.extend(quote! { + #[doc = #doc] + }) + } + + tokens + } + fn generate_enum_variants(&self, version: &ContainerVersion) -> TokenStream { let mut token_stream = TokenStream::new(); @@ -187,8 +178,8 @@ impl VersionedEnum { let next_module_name = &next_version.ident; let module_name = &version.ident; - let from_ident = &self.from_ident; - let enum_ident = &self.ident; + let enum_ident = &self.idents.original; + let from_ident = &self.idents.from; let mut variants = TokenStream::new(); diff --git a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs b/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs index f4c257e56..6dd493906 100644 --- a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs @@ -3,15 +3,12 @@ use std::ops::Deref; use itertools::Itertools; use proc_macro2::TokenStream; use quote::quote; -use syn::{DataStruct, Error, Ident}; +use syn::{parse_quote, DataStruct, Error, Ident}; use crate::{ attrs::common::ContainerAttributes, codegen::{ - common::{ - format_container_from_ident, Container, ContainerInput, ContainerVersion, Item, - VersionedContainer, - }, + common::{Container, ContainerInput, ContainerVersion, Item, VersionedContainer}, vstruct::field::VersionedField, }, }; @@ -39,11 +36,7 @@ impl Container for VersionedStruct { data: DataStruct, attributes: ContainerAttributes, ) -> syn::Result { - let ContainerInput { - original_attributes, - visibility, - ident, - } = input; + let ident = &input.ident; // Convert the raw version attributes into a container version. let versions: Vec<_> = (&attributes).into(); @@ -77,35 +70,44 @@ impl Container for VersionedStruct { } } - let from_ident = format_container_from_ident(&ident); - - Ok(Self(VersionedContainer { - skip_from: attributes - .options - .skip - .map_or(false, |s| s.from.is_present()), - original_attributes, - visibility, - from_ident, - versions, - items, - ident, - })) + // Validate K8s specific requirements + // Ensure that the struct name includes the 'Spec' suffix. + if attributes.kubernetes_attrs.is_some() && !ident.to_string().ends_with("Spec") { + return Err(Error::new( + ident.span(), + "struct name needs to include the `Spec` suffix if Kubernetes features are enabled via `#[versioned(k8s())]`" + )); + } + + Ok(Self(VersionedContainer::new( + input, attributes, versions, items, + ))) } fn generate_tokens(&self) -> TokenStream { - let mut token_stream = TokenStream::new(); + let mut kubernetes_crd_fn_calls = TokenStream::new(); + let mut container_definition = TokenStream::new(); + let mut versions = self.versions.iter().peekable(); while let Some(version) = versions.next() { - token_stream.extend(self.generate_version(version, versions.peek().copied())); + container_definition.extend(self.generate_version(version, versions.peek().copied())); + kubernetes_crd_fn_calls.extend(self.generate_kubernetes_crd_fn_call(version)); } - token_stream + // 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)); + } + + container_definition } } impl VersionedStruct { + /// Generates all tokens for a single instance of a versioned struct. fn generate_version( &self, version: &ContainerVersion, @@ -114,8 +116,8 @@ impl VersionedStruct { let mut token_stream = TokenStream::new(); let original_attributes = &self.original_attributes; + let struct_name = &self.idents.original; let visibility = &self.visibility; - let struct_name = &self.ident; // Generate fields of the struct for `version`. let fields = self.generate_struct_fields(version); @@ -131,19 +133,11 @@ impl VersionedStruct { .deprecated .then_some(quote! {#[deprecated = #deprecated_note]}); - let mut version_specific_docs = TokenStream::new(); - for (i, doc) in version.version_specific_docs.iter().enumerate() { - if i == 0 { - // Prepend an empty line to clearly separate the version - // specific docs. - version_specific_docs.extend(quote! { - #[doc = ""] - }) - } - version_specific_docs.extend(quote! { - #[doc = #doc] - }) - } + // Generate doc comments for the container (struct) + let version_specific_docs = self.generate_struct_docs(version); + + // Generate K8s specific code + let kubernetes_cr_derive = self.generate_kubernetes_cr_derive(version); // Generate tokens for the module and the contained struct token_stream.extend(quote! { @@ -152,8 +146,9 @@ impl VersionedStruct { #visibility mod #version_ident { use super::*; - #(#original_attributes)* #version_specific_docs + #(#original_attributes)* + #kubernetes_cr_derive pub struct #struct_name { #fields } @@ -161,40 +156,64 @@ impl VersionedStruct { }); // Generate the From impl between this `version` and the next one. - if !self.skip_from && !version.skip_from { + if !self.options.skip_from && !version.skip_from { token_stream.extend(self.generate_from_impl(version, next_version)); } token_stream } + /// Generates version specific doc comments for the struct. + fn generate_struct_docs(&self, version: &ContainerVersion) -> TokenStream { + let mut tokens = TokenStream::new(); + + for (i, doc) in version.version_specific_docs.iter().enumerate() { + if i == 0 { + // Prepend an empty line to clearly separate the version + // specific docs. + tokens.extend(quote! { + #[doc = ""] + }) + } + tokens.extend(quote! { + #[doc = #doc] + }) + } + + tokens + } + + /// Generates struct fields following the `name: type` format which includes + /// a trailing comma. fn generate_struct_fields(&self, version: &ContainerVersion) -> TokenStream { - let mut token_stream = TokenStream::new(); + let mut tokens = TokenStream::new(); for item in &self.items { - token_stream.extend(item.generate_for_container(version)); + tokens.extend(item.generate_for_container(version)); } - token_stream + tokens } + /// Generates the [`From`] impl which enables conversion between a version + /// and the next one. fn generate_from_impl( &self, version: &ContainerVersion, next_version: Option<&ContainerVersion>, - ) -> TokenStream { + ) -> Option { if let Some(next_version) = next_version { let next_module_name = &next_version.ident; let module_name = &version.ident; - let from_ident = &self.from_ident; - let struct_ident = &self.ident; + let struct_ident = &self.idents.original; + let from_ident = &self.idents.from; let fields = self.generate_from_fields(version, next_version, from_ident); // TODO (@Techassi): Be a little bit more clever about when to include // the #[allow(deprecated)] attribute. - return quote! { + return Some(quote! { #[automatically_derived] #[allow(deprecated)] impl From<#module_name::#struct_ident> for #next_module_name::#struct_ident { @@ -204,12 +223,14 @@ impl VersionedStruct { } } } - }; + }); } - quote! {} + None } + /// Generates fields used in the [`From`] impl following the + /// `new_name: struct_name.old_name` format which includes a trailing comma. fn generate_from_fields( &self, version: &ContainerVersion, @@ -225,3 +246,68 @@ impl VersionedStruct { token_stream } } + +// Kubernetes specific code generation +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()); + + return Some(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 { + let ident = &self.idents.kubernetes; + + quote! { + #[automatically_derived] + pub struct #ident; + + #[automatically_derived] + impl #ident { + /// Generates a merged CRD which contains all versions defined using the + /// `#[versioned()]` macro. + pub fn merged_crd( + stored_apiversion: &str + ) -> ::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) + } + } + } + } + + /// 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; + + let path: syn::Path = parse_quote!(#version_ident::#struct_ident); + return Some(quote! { + <#path as ::kube::CustomResourceExt>::crd(), + }); + } + + None + } +} diff --git a/crates/stackable-versioned-macros/tests/bad/README.md b/crates/stackable-versioned-macros/tests/default/fail/README.md similarity index 100% rename from crates/stackable-versioned-macros/tests/bad/README.md rename to crates/stackable-versioned-macros/tests/default/fail/README.md diff --git a/crates/stackable-versioned-macros/tests/bad/deprecate.rs b/crates/stackable-versioned-macros/tests/default/fail/deprecate.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/bad/deprecate.rs rename to crates/stackable-versioned-macros/tests/default/fail/deprecate.rs diff --git a/crates/stackable-versioned-macros/tests/bad/deprecate.stderr b/crates/stackable-versioned-macros/tests/default/fail/deprecate.stderr similarity index 75% rename from crates/stackable-versioned-macros/tests/bad/deprecate.stderr rename to crates/stackable-versioned-macros/tests/default/fail/deprecate.stderr index 1882ec773..c18cca62d 100644 --- a/crates/stackable-versioned-macros/tests/bad/deprecate.stderr +++ b/crates/stackable-versioned-macros/tests/default/fail/deprecate.stderr @@ -1,5 +1,5 @@ error: deprecation must be done using #[versioned(deprecated(since = "VERSION"))] - --> tests/bad/deprecate.rs:10:9 + --> tests/default/fail/deprecate.rs:10:9 | 10 | #[deprecated] | ^ diff --git a/crates/stackable-versioned-macros/tests/bad/skip_from_all.rs b/crates/stackable-versioned-macros/tests/default/fail/skip_from_all.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/bad/skip_from_all.rs rename to crates/stackable-versioned-macros/tests/default/fail/skip_from_all.rs diff --git a/crates/stackable-versioned-macros/tests/bad/skip_from_all.stderr b/crates/stackable-versioned-macros/tests/default/fail/skip_from_all.stderr similarity index 90% rename from crates/stackable-versioned-macros/tests/bad/skip_from_all.stderr rename to crates/stackable-versioned-macros/tests/default/fail/skip_from_all.stderr index 7115cbe49..622e3266b 100644 --- a/crates/stackable-versioned-macros/tests/bad/skip_from_all.stderr +++ b/crates/stackable-versioned-macros/tests/default/fail/skip_from_all.stderr @@ -1,5 +1,5 @@ error[E0308]: mismatched types - --> tests/bad/skip_from_all.rs:23:42 + --> tests/default/fail/skip_from_all.rs:23:42 | 23 | let foo_v1beta1 = v1beta1::Foo::from(foo_v1alpha1); | ------------------ ^^^^^^^^^^^^ expected `v1beta1::Foo`, found `v1alpha1::Foo` @@ -8,7 +8,7 @@ error[E0308]: mismatched types | = note: `v1alpha1::Foo` and `v1beta1::Foo` have similar names, but are actually distinct types note: `v1alpha1::Foo` is defined in module `crate::main::v1alpha1` of the current crate - --> tests/bad/skip_from_all.rs:4:5 + --> tests/default/fail/skip_from_all.rs:4:5 | 4 | / #[versioned( 5 | | version(name = "v1alpha1"), @@ -18,7 +18,7 @@ note: `v1alpha1::Foo` is defined in module `crate::main::v1alpha1` of the curren 9 | | )] | |______^ note: `v1beta1::Foo` is defined in module `crate::main::v1beta1` of the current crate - --> tests/bad/skip_from_all.rs:4:5 + --> tests/default/fail/skip_from_all.rs:4:5 | 4 | / #[versioned( 5 | | version(name = "v1alpha1"), diff --git a/crates/stackable-versioned-macros/tests/bad/skip_from_version.rs b/crates/stackable-versioned-macros/tests/default/fail/skip_from_version.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/bad/skip_from_version.rs rename to crates/stackable-versioned-macros/tests/default/fail/skip_from_version.rs diff --git a/crates/stackable-versioned-macros/tests/bad/skip_from_version.stderr b/crates/stackable-versioned-macros/tests/default/fail/skip_from_version.stderr similarity index 88% rename from crates/stackable-versioned-macros/tests/bad/skip_from_version.stderr rename to crates/stackable-versioned-macros/tests/default/fail/skip_from_version.stderr index 95e828bba..0fe75fea1 100644 --- a/crates/stackable-versioned-macros/tests/bad/skip_from_version.stderr +++ b/crates/stackable-versioned-macros/tests/default/fail/skip_from_version.stderr @@ -1,5 +1,5 @@ error[E0308]: mismatched types - --> tests/bad/skip_from_version.rs:23:32 + --> tests/default/fail/skip_from_version.rs:23:32 | 23 | let foo_v1 = v1::Foo::from(foo_v1beta1); | ------------- ^^^^^^^^^^^ expected `main::v1::Foo`, found `v1beta1::Foo` @@ -8,7 +8,7 @@ error[E0308]: mismatched types | = note: `v1beta1::Foo` and `main::v1::Foo` have similar names, but are actually distinct types note: `v1beta1::Foo` is defined in module `crate::main::v1beta1` of the current crate - --> tests/bad/skip_from_version.rs:4:5 + --> tests/default/fail/skip_from_version.rs:4:5 | 4 | / #[versioned( 5 | | version(name = "v1alpha1"), @@ -17,7 +17,7 @@ note: `v1beta1::Foo` is defined in module `crate::main::v1beta1` of the current 8 | | )] | |______^ note: `main::v1::Foo` is defined in module `crate::main::v1` of the current crate - --> tests/bad/skip_from_version.rs:4:5 + --> tests/default/fail/skip_from_version.rs:4:5 | 4 | / #[versioned( 5 | | version(name = "v1alpha1"), diff --git a/crates/stackable-versioned-macros/tests/good/README.md b/crates/stackable-versioned-macros/tests/default/pass/README.md similarity index 100% rename from crates/stackable-versioned-macros/tests/good/README.md rename to crates/stackable-versioned-macros/tests/default/pass/README.md diff --git a/crates/stackable-versioned-macros/tests/good/attributes_enum.rs b/crates/stackable-versioned-macros/tests/default/pass/attributes_enum.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/good/attributes_enum.rs rename to crates/stackable-versioned-macros/tests/default/pass/attributes_enum.rs diff --git a/crates/stackable-versioned-macros/tests/good/attributes_struct.rs b/crates/stackable-versioned-macros/tests/default/pass/attributes_struct.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/good/attributes_struct.rs rename to crates/stackable-versioned-macros/tests/default/pass/attributes_struct.rs diff --git a/crates/stackable-versioned-macros/tests/good/basic.rs b/crates/stackable-versioned-macros/tests/default/pass/basic.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/good/basic.rs rename to crates/stackable-versioned-macros/tests/default/pass/basic.rs diff --git a/crates/stackable-versioned-macros/tests/good/deprecate.rs b/crates/stackable-versioned-macros/tests/default/pass/deprecate.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/good/deprecate.rs rename to crates/stackable-versioned-macros/tests/default/pass/deprecate.rs diff --git a/crates/stackable-versioned-macros/tests/good/rename.rs b/crates/stackable-versioned-macros/tests/default/pass/rename.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/good/rename.rs rename to crates/stackable-versioned-macros/tests/default/pass/rename.rs diff --git a/crates/stackable-versioned-macros/tests/good/skip_from_version.rs b/crates/stackable-versioned-macros/tests/default/pass/skip_from_version.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/good/skip_from_version.rs rename to crates/stackable-versioned-macros/tests/default/pass/skip_from_version.rs diff --git a/crates/stackable-versioned-macros/tests/k8s/fail/crd.rs b/crates/stackable-versioned-macros/tests/k8s/fail/crd.rs new file mode 100644 index 000000000..38acfe8c8 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/k8s/fail/crd.rs @@ -0,0 +1,26 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use stackable_versioned_macros::versioned; + +#[allow(deprecated)] +fn main() { + #[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + version(name = "v1"), + k8s(group = "stackable.tech") + )] + #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] + pub struct Foo { + #[versioned( + added(since = "v1beta1"), + changed(since = "v1", from_name = "bah", from_type = "u16") + )] + bar: usize, + baz: bool, + } + + let merged_crd = Foo::merged_crd("v1").unwrap(); + println!("{}", serde_yaml::to_string(&merged_crd).unwrap()); +} diff --git a/crates/stackable-versioned-macros/tests/k8s/fail/crd.stderr b/crates/stackable-versioned-macros/tests/k8s/fail/crd.stderr new file mode 100644 index 000000000..33d671a9a --- /dev/null +++ b/crates/stackable-versioned-macros/tests/k8s/fail/crd.stderr @@ -0,0 +1,11 @@ +error: struct name needs to include the `Spec` suffix if Kubernetes features are enabled via `#[versioned(k8s())]` + --> tests/k8s/fail/crd.rs:15:16 + | +15 | pub struct Foo { + | ^^^ + +error[E0433]: failed to resolve: use of undeclared type `Foo` + --> tests/k8s/fail/crd.rs:24:22 + | +24 | let merged_crd = Foo::merged_crd("v1").unwrap(); + | ^^^ use of undeclared type `Foo` diff --git a/crates/stackable-versioned-macros/tests/k8s/pass/crd.rs b/crates/stackable-versioned-macros/tests/k8s/pass/crd.rs new file mode 100644 index 000000000..0defd8252 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/k8s/pass/crd.rs @@ -0,0 +1,26 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use stackable_versioned_macros::versioned; + +#[allow(deprecated)] +fn main() { + #[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + version(name = "v1"), + k8s(group = "stackable.tech") + )] + #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] + pub struct FooSpec { + #[versioned( + added(since = "v1beta1"), + changed(since = "v1", from_name = "bah", from_type = "u16") + )] + bar: usize, + baz: bool, + } + + let merged_crd = Foo::merged_crd("v1").unwrap(); + println!("{}", serde_yaml::to_string(&merged_crd).unwrap()); +} diff --git a/crates/stackable-versioned-macros/tests/trybuild.rs b/crates/stackable-versioned-macros/tests/trybuild.rs index 94ba753bc..dc0208c8c 100644 --- a/crates/stackable-versioned-macros/tests/trybuild.rs +++ b/crates/stackable-versioned-macros/tests/trybuild.rs @@ -7,34 +7,55 @@ //! //! [1]: https://github.com/dtolnay/trybuild?tab=readme-ov-file#workflow -// Enable the module below to get syntax highlighting and code completion. +// Enable the 'pass' module below to get syntax highlighting and code completion. // Adjust the list of modules to enable syntax highlighting and code completion. -// Unfortunately tests in subfolders aren't automatically included. +// Unfortunately tests in sub-folders aren't automatically included. // -// #[allow(dead_code)] -// mod good { -// mod attributes_enum; -// mod attributes_struct; -// mod basic; -// mod deprecate; -// mod rename; -// mod skip_from_version; -// } +// Similar to the above 'pass' module, enable the 'fail' module below to get +// syntax highlighting and code completion. You will need to comment them out +// again but before running tests, otherwise compilation will fail (as expected). +#[allow(dead_code)] +mod default { + // mod pass { + // mod attributes_enum; + // mod attributes_struct; + // mod basic; -// Similar to the above module, enable the module below to get syntax -// highlighting and code completion. You will need to comment them out again but -// before running tests, orherwise compilation will fail (as expected). -// -// #[allow(dead_code)] -// mod bad { -// mod deprecate; -// mod skip_from_all; -// mod skip_from_version; -// } + // mod deprecate; + // mod rename; + // mod skip_from_version; + // } + + // mod fail { + // mod deprecate; + // mod skip_from_all; + // mod skip_from_version; + // } +} + +#[test] +fn default_macros() { + let t = trybuild::TestCases::new(); + t.pass("tests/default/pass/*.rs"); + t.compile_fail("tests/default/fail/*.rs"); +} + +#[cfg(feature = "k8s")] +#[allow(dead_code)] +mod k8s { + // mod pass { + // mod crd; + // } + + // mod fail { + // mod crd; + // } +} +#[cfg(feature = "k8s")] #[test] -fn macros() { +fn k8s_macros() { let t = trybuild::TestCases::new(); - t.pass("tests/good/*.rs"); - t.compile_fail("tests/bad/*.rs"); + t.pass("tests/k8s/pass/*.rs"); + t.compile_fail("tests/k8s/fail/*.rs"); } diff --git a/crates/stackable-versioned/CHANGELOG.md b/crates/stackable-versioned/CHANGELOG.md index a760544d8..3161f9512 100644 --- a/crates/stackable-versioned/CHANGELOG.md +++ b/crates/stackable-versioned/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. - Pass through container and item attributes (including doc-comments). Add attribute for version specific docs ([#847]). - Forward container visibility to generated modules ([#850]). +- Add support for Kubernetes-specific features ([#857]). - Add `use super::*` to version modules to be able to use imported types ([#859]). @@ -17,6 +18,7 @@ All notable changes to this project will be documented in this file. - BREAKING: Rename `renamed()` action to `changed()` and renamed `from` parameter to `from_name` ([#844]). +- Bump syn to 2.0.77 ([#857]). ### Fixed @@ -28,6 +30,7 @@ All notable changes to this project will be documented in this file. [#844]: https://github.com/stackabletech/operator-rs/pull/844 [#847]: https://github.com/stackabletech/operator-rs/pull/847 [#850]: https://github.com/stackabletech/operator-rs/pull/850 +[#857]: https://github.com/stackabletech/operator-rs/pull/857 [#859]: https://github.com/stackabletech/operator-rs/pull/859 [#860]: https://github.com/stackabletech/operator-rs/pull/860