From 0d78554e457a35c4b2d629fdd924cde43ac4bdc4 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 13 Jun 2025 13:34:17 +0200 Subject: [PATCH 01/16] chore: Add edition to rustfmt override command --- .vscode/settings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 8c9fdee9f..2571fe27f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,8 @@ "rust-analyzer.rustfmt.overrideCommand": [ "rustfmt", "+nightly-2025-05-26", + "--edition", + "2024", "--" ] } From c3d48aeeb7a65862f34a7cc6a2a812a62c71dada Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 17 Jun 2025 12:29:38 +0200 Subject: [PATCH 02/16] fix(stackable-versioned): Finish error accumulator This uses correct error handling by letting the error accumulator handle the fallible function. Previously, this panicked, because the accumulator was dropped without being finished. --- .../src/codegen/module.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/stackable-versioned-macros/src/codegen/module.rs b/crates/stackable-versioned-macros/src/codegen/module.rs index c801de4a3..2a77afa49 100644 --- a/crates/stackable-versioned-macros/src/codegen/module.rs +++ b/crates/stackable-versioned-macros/src/codegen/module.rs @@ -60,12 +60,18 @@ impl Module { for item in items { match item { Item::Enum(item_enum) => { - let container = Container::new_enum_nested(item_enum, &versions)?; - containers.push(container); + if let Some(container) = + errors.handle(Container::new_enum_nested(item_enum, &versions)) + { + containers.push(container); + }; } Item::Struct(item_struct) => { - let container = Container::new_struct_nested(item_struct, &versions)?; - containers.push(container); + if let Some(container) = + errors.handle(Container::new_struct_nested(item_struct, &versions)) + { + containers.push(container); + } } Item::Mod(submodule) => { if !versions From 0f1e0a59f9e09727c317692df79e4ddf4a820985 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 17 Jun 2025 13:01:29 +0200 Subject: [PATCH 03/16] refactor(stackable-versioned): Simplify and improve From code generation --- .../src/codegen/container/enum.rs | 171 ++++++----------- .../src/codegen/container/mod.rs | 43 ++--- .../src/codegen/container/struct/mod.rs | 178 +++++++----------- .../src/codegen/item/field.rs | 118 ++++-------- .../src/codegen/item/variant.rs | 70 +------ .../src/codegen/module.rs | 11 +- 6 files changed, 202 insertions(+), 389 deletions(-) diff --git a/crates/stackable-versioned-macros/src/codegen/container/enum.rs b/crates/stackable-versioned-macros/src/codegen/container/enum.rs index 278145435..7ca6efb98 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/enum.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/enum.rs @@ -10,7 +10,7 @@ use crate::{ codegen::{ ItemStatus, StandaloneContainerAttributes, VersionDefinition, changes::Neighbors, - container::{CommonContainerData, Container, ContainerIdents, ContainerOptions}, + container::{CommonContainerData, Container, ContainerIdents, ContainerOptions, Direction}, item::VersionedVariant, }, }; @@ -122,9 +122,10 @@ impl Enum { } } - /// Generates code for the `From for NextVersion` implementation. - pub fn generate_upgrade_from_impl( + // TODO (@Techassi): Add doc comments + pub fn generate_from_impl( &self, + direction: Direction, version: &VersionDefinition, next_version: Option<&VersionDefinition>, add_attributes: bool, @@ -133,119 +134,69 @@ impl Enum { return None; } - match next_version { - Some(next_version) => { - // TODO (@Techassi): Support generic types which have been removed in newer versions, - // but need to exist for older versions How do we represent that? Because the - // defined struct always represents the latest version. I guess we could generally - // advise against using generic types, but if you have to, avoid removing it in - // later versions. - let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl(); - let from_enum_ident = &self.common.idents.parameter; - let enum_ident = &self.common.idents.original; - - let for_module_ident = &next_version.idents.module; - let from_module_ident = &version.idents.module; - - let variants: TokenStream = self - .variants - .iter() - .filter_map(|v| { - v.generate_for_upgrade_from_impl(version, next_version, enum_ident) - }) - .collect(); - - // Include allow(deprecated) only when this or the next version is - // deprecated. Also include it, when a variant in this or the next - // version is deprecated. - let allow_attribute = (version.deprecated.is_some() - || next_version.deprecated.is_some() - || self.is_any_variant_deprecated(version) - || self.is_any_variant_deprecated(next_version)) - .then_some(quote! { #[allow(deprecated)] }); - - // Only add the #[automatically_derived] attribute only if this impl is used - // outside of a module (in standalone mode). - let automatically_derived = add_attributes - .not() - .then(|| quote! {#[automatically_derived]}); - - Some(quote! { - #automatically_derived - #allow_attribute - impl #impl_generics ::std::convert::From<#from_module_ident::#enum_ident #type_generics> for #for_module_ident::#enum_ident #type_generics - #where_clause - { - fn from(#from_enum_ident: #from_module_ident::#enum_ident #type_generics) -> Self { - match #from_enum_ident { - #variants - } - } - } - }) - } - None => None, - } - } - - pub fn generate_downgrade_from_impl( - &self, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - add_attributes: bool, - ) -> Option { - if version.skip_from || self.common.options.skip_from { - return None; - } - - match next_version { - Some(next_version) => { - let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl(); - let from_enum_ident = &self.common.idents.parameter; - let enum_ident = &self.common.idents.original; - - let from_module_ident = &next_version.idents.module; - let for_module_ident = &version.idents.module; - - let variants: TokenStream = self - .variants + next_version.map(|next_version| { + // TODO (@Techassi): Support generic types which have been removed in newer versions, + // but need to exist for older versions How do we represent that? Because the + // defined struct always represents the latest version. I guess we could generally + // advise against using generic types, but if you have to, avoid removing it in + // later versions. + let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl(); + let from_enum_ident = &self.common.idents.parameter; + let enum_ident = &self.common.idents.original; + + // Include allow(deprecated) only when this or the next version is + // deprecated. Also include it, when a variant in this or the next + // version is deprecated. + let allow_attribute = (version.deprecated.is_some() + || next_version.deprecated.is_some() + || self.is_any_variant_deprecated(version) + || self.is_any_variant_deprecated(next_version)) + .then_some(quote! { #[allow(deprecated)] }); + + // Only add the #[automatically_derived] attribute only if this impl is used + // outside of a module (in standalone mode). + let automatically_derived = add_attributes + .not() + .then(|| quote! {#[automatically_derived]}); + + let variants = |direction: Direction| -> TokenStream { + self.variants .iter() .filter_map(|v| { - v.generate_for_downgrade_from_impl(version, next_version, enum_ident) + v.generate_for_from_impl(direction, version, next_version, enum_ident) }) - .collect(); - - // Include allow(deprecated) only when this or the next version is - // deprecated. Also include it, when a variant in this or the next - // version is deprecated. - let allow_attribute = (version.deprecated.is_some() - || next_version.deprecated.is_some() - || self.is_any_variant_deprecated(version) - || self.is_any_variant_deprecated(next_version)) - .then_some(quote! { #[allow(deprecated)] }); - - // Only add the #[automatically_derived] attribute only if this impl is used - // outside of a module (in standalone mode). - let automatically_derived = add_attributes - .not() - .then(|| quote! {#[automatically_derived]}); - - Some(quote! { - #automatically_derived - #allow_attribute - impl #impl_generics ::std::convert::From<#from_module_ident::#enum_ident #type_generics> for #for_module_ident::#enum_ident #type_generics - #where_clause - { - fn from(#from_enum_ident: #from_module_ident::#enum_ident #type_generics) -> Self { - match #from_enum_ident { - #variants - } + .collect() + }; + + let (variants, for_module_ident, from_module_ident) = match direction { + Direction::Upgrade => { + let for_module_ident = &next_version.idents.module; + let from_module_ident = &version.idents.module; + + (variants(Direction::Upgrade), for_module_ident, from_module_ident) + }, + Direction::Downgrade => { + let for_module_ident = &version.idents.module; + let from_module_ident = &next_version.idents.module; + + (variants(Direction::Downgrade), for_module_ident, from_module_ident) + }, + }; + + quote! { + #automatically_derived + #allow_attribute + impl #impl_generics ::std::convert::From<#from_module_ident::#enum_ident #type_generics> for #for_module_ident::#enum_ident #type_generics + #where_clause + { + fn from(#from_enum_ident: #from_module_ident::#enum_ident #type_generics) -> Self { + match #from_enum_ident { + #variants } } - }) + } } - None => None, - } + }) } /// Returns whether any variant is deprecated in the provided `version`. diff --git a/crates/stackable-versioned-macros/src/codegen/container/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/mod.rs index ed72fbe46..8699e6738 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/mod.rs @@ -46,36 +46,20 @@ impl Container { } } - /// Generates the `From for NextVersion` implementation for the container. - pub fn generate_upgrade_from_impl( + pub fn generate_from_impl( &self, + direction: Direction, version: &VersionDefinition, next_version: Option<&VersionDefinition>, add_attributes: bool, ) -> Option { match self { Container::Struct(s) => { - s.generate_upgrade_from_impl(version, next_version, add_attributes) + // TODO (@Techassi): Decide here (based on K8s args) what we want to generate + s.generate_from_impl(direction, version, next_version, add_attributes) } Container::Enum(e) => { - e.generate_upgrade_from_impl(version, next_version, add_attributes) - } - } - } - - /// Generates the `From for Version` implementation for the container. - pub fn generate_downgrade_from_impl( - &self, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - add_attributes: bool, - ) -> Option { - match self { - Container::Struct(s) => { - s.generate_downgrade_from_impl(version, next_version, add_attributes) - } - Container::Enum(e) => { - e.generate_downgrade_from_impl(version, next_version, add_attributes) + e.generate_from_impl(direction, version, next_version, add_attributes) } } } @@ -183,12 +167,15 @@ impl StandaloneContainer { // Generate the From impl for upgrading the CRD. let upgrade_from_impl = self.container - .generate_upgrade_from_impl(version, next_version, false); + .generate_from_impl(Direction::Upgrade, version, next_version, false); // Generate the From impl for downgrading the CRD. - let downgrade_from_impl = - self.container - .generate_downgrade_from_impl(version, next_version, false); + let downgrade_from_impl = self.container.generate_from_impl( + Direction::Downgrade, + version, + next_version, + false, + ); // Add the #[deprecated] attribute when the version is marked as deprecated. let deprecated_attribute = version @@ -287,3 +274,9 @@ pub struct ContainerOptions { pub kubernetes_arguments: Option, pub skip_from: bool, } + +#[derive(Copy, Clone, Debug)] +pub enum Direction { + Upgrade, + Downgrade, +} diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs index ae13fe21c..1bdbb25ad 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs @@ -10,7 +10,7 @@ use crate::{ codegen::{ ItemStatus, StandaloneContainerAttributes, VersionDefinition, changes::Neighbors, - container::{CommonContainerData, Container, ContainerIdents, ContainerOptions}, + container::{CommonContainerData, Container, ContainerIdents, ContainerOptions, Direction}, item::VersionedField, }, }; @@ -161,14 +161,14 @@ impl Struct { } } - // TODO (@Techassi): It looks like some of the stuff from the upgrade and downgrade functions - // can be combined into a single piece of code. Figure out a nice way to do that. + // TODO (@Techassi): Adjust doc comment /// Generates code for the `From for NextVersion` implementation. /// /// The `add_attributes` parameter declares if attributes (macros) should be added to the /// generated `From` impl block. - pub fn generate_upgrade_from_impl( + pub fn generate_from_impl( &self, + direction: Direction, version: &VersionDefinition, next_version: Option<&VersionDefinition>, add_attributes: bool, @@ -177,119 +177,75 @@ impl Struct { return None; } - match next_version { - Some(next_version) => { - // TODO (@Techassi): Support generic types which have been removed in newer versions, - // but need to exist for older versions How do we represent that? Because the - // defined struct always represents the latest version. I guess we could generally - // advise against using generic types, but if you have to, avoid removing it in - // later versions. - let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl(); - let from_struct_ident = &self.common.idents.parameter; - let struct_ident = &self.common.idents.original; - - let for_module_ident = &next_version.idents.module; - let from_module_ident = &version.idents.module; - - let fields: TokenStream = self + next_version.map(|next_version| { + // TODO (@Techassi): Support generic types which have been removed in newer versions, + // but need to exist for older versions How do we represent that? Because the + // defined struct always represents the latest version. I guess we could generally + // advise against using generic types, but if you have to, avoid removing it in + // later versions. + let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl(); + let from_struct_ident = &self.common.idents.parameter; + let struct_ident = &self.common.idents.original; + + // Include allow(deprecated) only when this or the next version is + // deprecated. Also include it, when a field in this or the next + // version is deprecated. + let allow_attribute = (version.deprecated.is_some() + || next_version.deprecated.is_some() + || self.is_any_field_deprecated(version) + || self.is_any_field_deprecated(next_version)) + .then(|| quote! { #[allow(deprecated)] }); + + // Only add the #[automatically_derived] attribute only if this impl is used + // outside of a module (in standalone mode). + let automatically_derived = add_attributes + .not() + .then(|| quote! {#[automatically_derived]}); + + let fields = |direction: Direction| -> TokenStream { + self .fields .iter() - .map(|f| { - f.generate_for_upgrade_from_impl(version, next_version, from_struct_ident) + .filter_map(|f| { + f.generate_for_from_impl( + direction, + version, + next_version, + from_struct_ident, + ) }) - .collect(); - - // Include allow(deprecated) only when this or the next version is - // deprecated. Also include it, when a field in this or the next - // version is deprecated. - let allow_attribute = (version.deprecated.is_some() - || next_version.deprecated.is_some() - || self.is_any_field_deprecated(version) - || self.is_any_field_deprecated(next_version)) - .then(|| quote! { #[allow(deprecated)] }); - - // Only add the #[automatically_derived] attribute only if this impl is used - // outside of a module (in standalone mode). - let automatically_derived = add_attributes - .not() - .then(|| quote! {#[automatically_derived]}); - - Some(quote! { - #automatically_derived - #allow_attribute - impl #impl_generics ::std::convert::From<#from_module_ident::#struct_ident #type_generics> for #for_module_ident::#struct_ident #type_generics - #where_clause - { - fn from(#from_struct_ident: #from_module_ident::#struct_ident #type_generics) -> Self { - Self { - #fields - } - } - } - }) - } - None => None, - } - } - - pub fn generate_downgrade_from_impl( - &self, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - add_attributes: bool, - ) -> Option { - if version.skip_from || self.common.options.skip_from { - return None; - } - - match next_version { - Some(next_version) => { - let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl(); - let from_struct_ident = &self.common.idents.parameter; - let struct_ident = &self.common.idents.original; - - let from_module_ident = &next_version.idents.module; - let for_module_ident = &version.idents.module; - - let fields: TokenStream = self - .fields - .iter() - .map(|f| { - f.generate_for_downgrade_from_impl(version, next_version, from_struct_ident) - }) - .collect(); - - // Include allow(deprecated) only when this or the next version is - // deprecated. Also include it, when a field in this or the next - // version is deprecated. - let allow_attribute = (version.deprecated.is_some() - || next_version.deprecated.is_some() - || self.is_any_field_deprecated(version) - || self.is_any_field_deprecated(next_version)) - .then(|| quote! { #[allow(deprecated)] }); - - // Only add the #[automatically_derived] attribute only if this impl is used - // outside of a module (in standalone mode). - let automatically_derived = add_attributes - .not() - .then(|| quote! {#[automatically_derived]}); - - Some(quote! { - #automatically_derived - #allow_attribute - impl #impl_generics ::std::convert::From<#from_module_ident::#struct_ident #type_generics> for #for_module_ident::#struct_ident #type_generics - #where_clause - { - fn from(#from_struct_ident: #from_module_ident::#struct_ident #type_generics) -> Self { - Self { - #fields - } + .collect() + }; + + let (fields, for_module_ident, from_module_ident) = match direction { + Direction::Upgrade => { + let from_module_ident = &version.idents.module; + let for_module_ident = &next_version.idents.module; + + (fields(Direction::Upgrade), for_module_ident, from_module_ident) + } + Direction::Downgrade => { + let from_module_ident = &next_version.idents.module; + let for_module_ident = &version.idents.module; + + (fields(Direction::Downgrade), for_module_ident, from_module_ident) + } + }; + + quote! { + #automatically_derived + #allow_attribute + impl #impl_generics ::std::convert::From<#from_module_ident::#struct_ident #type_generics> for #for_module_ident::#struct_ident #type_generics + #where_clause + { + fn from(#from_struct_ident: #from_module_ident::#struct_ident #type_generics) -> Self { + Self { + #fields } } - }) + } } - None => None, - } + }) } /// Returns whether any field is deprecated in the provided `version`. diff --git a/crates/stackable-versioned-macros/src/codegen/item/field.rs b/crates/stackable-versioned-macros/src/codegen/item/field.rs index 358a94000..9cfd403ba 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/field.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/field.rs @@ -11,6 +11,7 @@ use crate::{ codegen::{ ItemStatus, VersionDefinition, changes::{BTreeMapExt, ChangesetExt}, + container::Direction, }, utils::FieldIdent, }; @@ -140,13 +141,13 @@ impl VersionedField { } } - // TODO (@Techassi): This should probably return an optional token stream. - pub fn generate_for_upgrade_from_impl( + pub fn generate_for_from_impl( &self, + direction: Direction, version: &VersionDefinition, next_version: &VersionDefinition, from_struct_ident: &IdentString, - ) -> TokenStream { + ) -> Option { match &self.changes { Some(changes) => { let next_change = changes.get_expect(&next_version.inner); @@ -156,93 +157,45 @@ impl VersionedField { // If both this status and the next one is NotPresent, which means // a field was introduced after a bunch of versions, we don't // need to generate any code for the From impl. - (ItemStatus::NotPresent, ItemStatus::NotPresent) => { - quote! {} - } + (ItemStatus::NotPresent, ItemStatus::NotPresent) => None, ( _, ItemStatus::Addition { ident, default_fn, .. }, - ) => quote! { - #ident: #default_fn(), + ) => match direction { + Direction::Upgrade => Some(quote! { #ident: #default_fn(), }), + Direction::Downgrade => None, }, - ( - _, - ItemStatus::Change { - from_ident: old_field_ident, - upgrade_with, - to_ident, - .. - }, - ) => match upgrade_with { - // The user specified a custom conversion function which - // will be used here instead of the default .into() call - // which utilizes From impls. - Some(upgrade_fn) => quote! { - #to_ident: #upgrade_fn(#from_struct_ident.#old_field_ident), - }, - // Default .into() call using From impls. - None => quote! { - #to_ident: #from_struct_ident.#old_field_ident.into(), - }, - }, - (old, next) => { - let next_field_ident = next.get_ident(); - let old_field_ident = old.get_ident(); - - // NOTE (@Techassi): Do we really need .into() here. I'm - // currently not sure why it is there and if it is needed - // in some edge cases. - quote! { - #next_field_ident: #from_struct_ident.#old_field_ident.into(), - } - } - } - } - None => { - let field_ident = &*self.ident; - - quote! { - #field_ident: #from_struct_ident.#field_ident.into(), - } - } - } - } - - pub fn generate_for_downgrade_from_impl( - &self, - version: &VersionDefinition, - next_version: &VersionDefinition, - from_struct_ident: &IdentString, - ) -> TokenStream { - match &self.changes { - Some(changes) => { - let next_change = changes.get_expect(&next_version.inner); - let change = changes.get_expect(&version.inner); - - match (change, next_change) { - // If both this status and the next one is NotPresent, which means - // a field was introduced after a bunch of versions, we don't - // need to generate any code for the From impl. - (ItemStatus::NotPresent, ItemStatus::NotPresent) => { - quote! {} - } - (_, ItemStatus::Addition { .. }) => quote! {}, ( _, ItemStatus::Change { downgrade_with, - from_ident: old_field_ident, + upgrade_with, + from_ident, to_ident, .. }, - ) => match downgrade_with { - Some(downgrade_fn) => quote! { - #old_field_ident: #downgrade_fn(#from_struct_ident.#to_ident), + ) => match direction { + Direction::Upgrade => match upgrade_with { + // The user specified a custom conversion function which + // will be used here instead of the default .into() call + // which utilizes From impls. + Some(upgrade_fn) => Some(quote! { + #to_ident: #upgrade_fn(#from_struct_ident.#from_ident), + }), + // Default .into() call using From impls. + None => Some(quote! { + #to_ident: #from_struct_ident.#from_ident.into(), + }), }, - None => quote! { - #old_field_ident: #from_struct_ident.#to_ident.into(), + Direction::Downgrade => match downgrade_with { + Some(downgrade_fn) => Some(quote! { + #from_ident: #downgrade_fn(#from_struct_ident.#to_ident), + }), + None => Some(quote! { + #from_ident: #from_struct_ident.#to_ident.into(), + }), }, }, (old, next) => { @@ -252,8 +205,13 @@ impl VersionedField { // NOTE (@Techassi): Do we really need .into() here. I'm // currently not sure why it is there and if it is needed // in some edge cases. - quote! { - #old_field_ident: #from_struct_ident.#next_field_ident.into(), + match direction { + Direction::Upgrade => Some(quote! { + #next_field_ident: #from_struct_ident.#old_field_ident.into(), + }), + Direction::Downgrade => Some(quote! { + #old_field_ident: #from_struct_ident.#next_field_ident.into(), + }), } } } @@ -261,9 +219,9 @@ impl VersionedField { None => { let field_ident = &*self.ident; - quote! { + Some(quote! { #field_ident: #from_struct_ident.#field_ident.into(), - } + }) } } } diff --git a/crates/stackable-versioned-macros/src/codegen/item/variant.rs b/crates/stackable-versioned-macros/src/codegen/item/variant.rs index b9c30f750..30dd188d8 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/variant.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/variant.rs @@ -11,6 +11,7 @@ use crate::{ codegen::{ ItemStatus, VersionDefinition, changes::{BTreeMapExt, ChangesetExt}, + container::Direction, }, utils::VariantIdent, }; @@ -130,8 +131,9 @@ impl VersionedVariant { } } - pub fn generate_for_upgrade_from_impl( + pub fn generate_for_from_impl( &self, + direction: Direction, version: &VersionDefinition, next_version: &VersionDefinition, enum_ident: &IdentString, @@ -159,9 +161,10 @@ impl VersionedVariant { #next_version_ident::#enum_ident::#next_variant_ident #variant_fields }; - Some(quote! { - #old => #next, - }) + match direction { + Direction::Upgrade => Some(quote! { #old => #next, }), + Direction::Downgrade => Some(quote! { #next => #old, }), + } } } } @@ -177,64 +180,11 @@ impl VersionedVariant { #next_version_ident::#enum_ident::#variant_ident #variant_fields }; - Some(quote! { - #old => #next, - }) - } - } - } - - pub fn generate_for_downgrade_from_impl( - &self, - version: &VersionDefinition, - next_version: &VersionDefinition, - enum_ident: &IdentString, - ) -> Option { - let variant_fields = self.fields_as_token_stream(); - - match &self.changes { - Some(changes) => { - let next_change = changes.get_expect(&next_version.inner); - let change = changes.get_expect(&version.inner); - - match (change, next_change) { - (_, ItemStatus::Addition { .. }) => None, - (old, next) => { - let next_version_ident = &next_version.idents.module; - let old_version_ident = &version.idents.module; - - let next_variant_ident = next.get_ident(); - let old_variant_ident = old.get_ident(); - - let old = quote! { - #old_version_ident::#enum_ident::#old_variant_ident #variant_fields - }; - let next = quote! { - #next_version_ident::#enum_ident::#next_variant_ident #variant_fields - }; - - Some(quote! { - #next => #old, - }) - } + match direction { + Direction::Upgrade => Some(quote! { #old => #next, }), + Direction::Downgrade => Some(quote! { #next => #old, }), } } - None => { - let next_version_ident = &next_version.idents.module; - let old_version_ident = &version.idents.module; - let variant_ident = &*self.ident; - - let old = quote! { - #old_version_ident::#enum_ident::#variant_ident #variant_fields - }; - let next = quote! { - #next_version_ident::#enum_ident::#variant_ident #variant_fields - }; - - Some(quote! { - #next => #old, - }) - } } } diff --git a/crates/stackable-versioned-macros/src/codegen/module.rs b/crates/stackable-versioned-macros/src/codegen/module.rs index 2a77afa49..9fcfa863b 100644 --- a/crates/stackable-versioned-macros/src/codegen/module.rs +++ b/crates/stackable-versioned-macros/src/codegen/module.rs @@ -7,7 +7,10 @@ use syn::{Ident, Item, ItemMod, ItemUse, Visibility, token::Pub}; use crate::{ ModuleAttributes, - codegen::{KubernetesTokens, VersionDefinition, container::Container}, + codegen::{ + KubernetesTokens, VersionDefinition, + container::{Container, Direction}, + }, }; /// A versioned module. @@ -172,13 +175,15 @@ impl Module { container_definitions.extend(container.generate_definition(version)); if !self.skip_from { - from_impls.extend(container.generate_upgrade_from_impl( + from_impls.extend(container.generate_from_impl( + Direction::Upgrade, version, next_version, self.preserve_module, )); - from_impls.extend(container.generate_downgrade_from_impl( + from_impls.extend(container.generate_from_impl( + Direction::Downgrade, version, next_version, self.preserve_module, From a86bf17c2bf0a3f2f4f75887be823e1ebc9a9dc5 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 26 Jun 2025 16:35:18 +0200 Subject: [PATCH 04/16] refactor(stackable-versioned): Remove standalone and non-K8s specific code --- crates/stackable-operator/Cargo.toml | 2 +- crates/stackable-versioned-macros/Cargo.toml | 10 +- .../src/attrs/common.rs | 127 --- .../src/attrs/container.rs | 76 ++ .../src/attrs/container/k8s.rs | 165 ---- .../src/attrs/container/mod.rs | 72 -- .../src/attrs/mod.rs | 1 - .../src/attrs/module.rs | 260 +++++- .../src/codegen/container/enum.rs | 41 +- .../src/codegen/container/mod.rs | 171 +--- .../src/codegen/container/struct/k8s.rs | 742 ------------------ .../src/codegen/container/struct/mod.rs | 62 +- .../src/codegen/mod.rs | 28 +- crates/stackable-versioned-macros/src/lib.rs | 563 +++++++------ .../tests/trybuild.rs | 4 +- crates/stackable-versioned/Cargo.toml | 24 +- crates/stackable-versioned/src/k8s.rs | 113 --- crates/stackable-versioned/src/lib.rs | 148 +++- 18 files changed, 802 insertions(+), 1807 deletions(-) delete mode 100644 crates/stackable-versioned-macros/src/attrs/common.rs create mode 100644 crates/stackable-versioned-macros/src/attrs/container.rs delete mode 100644 crates/stackable-versioned-macros/src/attrs/container/k8s.rs delete mode 100644 crates/stackable-versioned-macros/src/attrs/container/mod.rs delete mode 100644 crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs delete mode 100644 crates/stackable-versioned/src/k8s.rs diff --git a/crates/stackable-operator/Cargo.toml b/crates/stackable-operator/Cargo.toml index af065061f..b15574e6e 100644 --- a/crates/stackable-operator/Cargo.toml +++ b/crates/stackable-operator/Cargo.toml @@ -16,8 +16,8 @@ versioned = [] [dependencies] stackable-telemetry = { path = "../stackable-telemetry", features = ["clap"] } -stackable-versioned = { path = "../stackable-versioned", features = ["k8s"] } stackable-operator-derive = { path = "../stackable-operator-derive" } +stackable-versioned = { path = "../stackable-versioned" } stackable-shared = { path = "../stackable-shared" } chrono.workspace = true diff --git a/crates/stackable-versioned-macros/Cargo.toml b/crates/stackable-versioned-macros/Cargo.toml index fe6fe91a4..1712151a0 100644 --- a/crates/stackable-versioned-macros/Cargo.toml +++ b/crates/stackable-versioned-macros/Cargo.toml @@ -24,10 +24,6 @@ normal = ["k8s-openapi", "kube"] [lib] proc-macro = true -[features] -full = ["k8s"] -k8s = ["dep:kube", "dep:k8s-openapi"] - [dependencies] k8s-version = { path = "../k8s-version", features = ["darling"] } @@ -35,15 +31,15 @@ convert_case.workspace = true darling.workspace = true indoc.workspace = true itertools.workspace = true -k8s-openapi = { workspace = true, optional = true } -kube = { workspace = true, optional = true } +k8s-openapi.workspace = true +kube.workspace = true proc-macro2.workspace = true syn.workspace = true quote.workspace = true [dev-dependencies] # Only needed for doc tests / examples -stackable-versioned = { path = "../stackable-versioned", features = ["k8s"] } +stackable-versioned = { path = "../stackable-versioned" } insta.workspace = true prettyplease.workspace = true diff --git a/crates/stackable-versioned-macros/src/attrs/common.rs b/crates/stackable-versioned-macros/src/attrs/common.rs deleted file mode 100644 index 24ee40e87..000000000 --- a/crates/stackable-versioned-macros/src/attrs/common.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::ops::Deref; - -use darling::{ - Error, FromMeta, Result, - util::{Flag, Override as FlagOrOverride, SpannedValue}, -}; -use itertools::Itertools; -use k8s_version::Version; - -pub trait CommonOptions { - fn allow_unsorted(&self) -> Flag; -} - -#[derive(Debug, FromMeta)] -#[darling(and_then = CommonRootArguments::validate)] -pub struct CommonRootArguments -where - T: CommonOptions + Default, -{ - #[darling(default)] - pub options: T, - - #[darling(multiple, rename = "version")] - pub versions: SpannedValue>, -} - -impl CommonRootArguments -where - T: CommonOptions + Default, -{ - fn validate(mut self) -> Result { - let mut errors = Error::accumulator(); - - if self.versions.is_empty() { - errors.push( - Error::custom("at least one or more `version`s must be defined") - .with_span(&self.versions.span()), - ); - } - - let is_sorted = self.versions.iter().is_sorted_by_key(|v| v.name); - - // It needs to be sorted, even though the definition could be unsorted - // (if allow_unsorted is set). - self.versions.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); - - if !self.options.allow_unsorted().is_present() && !is_sorted { - let versions = self.versions.iter().map(|v| v.name).join(", "); - - errors.push(Error::custom(format!( - "versions must be defined in ascending order: {versions}", - ))); - } - - let duplicate_versions: Vec<_> = self - .versions - .iter() - .duplicates_by(|v| v.name) - .map(|v| v.name) - .collect(); - - if !duplicate_versions.is_empty() { - let versions = duplicate_versions.iter().join(", "); - - errors.push(Error::custom(format!( - "contains duplicate versions: {versions}", - ))); - } - - errors.finish_with(self) - } -} - -/// This struct contains supported version arguments. -/// -/// Supported arguments are: -/// -/// - `name` of the version, like `v1alpha1`. -/// - `deprecated` flag to mark that version as deprecated. -/// - `skip` option to skip generating various pieces of code. -/// - `doc` option to add version-specific documentation. -#[derive(Clone, Debug, FromMeta)] -pub struct VersionArguments { - pub deprecated: Option>, - pub skip: Option, - pub doc: Option, - pub name: Version, -} - -/// This struct contains supported common skip arguments. -/// -/// Supported arguments are: -/// -/// - `from` flag, which skips generating [`From`] implementations when provided. -#[derive(Clone, Debug, Default, FromMeta)] -pub struct SkipArguments { - /// Whether the [`From`] implementation generation should be skipped for all versions of this - /// container. - pub from: Flag, -} - -/// Wraps a value to indicate whether it is original or has been overridden. -#[derive(Clone, Debug)] -pub enum Override { - Default(T), - Explicit(T), -} - -impl FromMeta for Override -where - T: FromMeta, -{ - fn from_meta(item: &syn::Meta) -> Result { - FromMeta::from_meta(item).map(Override::Explicit) - } -} - -impl Deref for Override { - type Target = T; - - fn deref(&self) -> &Self::Target { - match &self { - Override::Default(inner) => inner, - Override::Explicit(inner) => inner, - } - } -} diff --git a/crates/stackable-versioned-macros/src/attrs/container.rs b/crates/stackable-versioned-macros/src/attrs/container.rs new file mode 100644 index 000000000..d75045ec6 --- /dev/null +++ b/crates/stackable-versioned-macros/src/attrs/container.rs @@ -0,0 +1,76 @@ +use darling::{Error, FromAttributes, FromMeta, Result, util::Flag}; +use syn::Path; + +#[derive(Debug, FromAttributes)] +#[darling(attributes(versioned), and_then = ContainerAttributes::validate)] +pub struct ContainerAttributes { + #[darling(rename = "crd")] + pub crd_arguments: Option, + + #[darling(default)] + pub skip: ContainerSkipArguments, +} + +impl ContainerAttributes { + fn validate(self) -> Result { + if self.crd_arguments.is_some() + && (self.skip.object_from.is_present() + || self.skip.merged_crd.is_present() + || self.skip.try_convert.is_present()) + { + return Err(Error::custom("spec sub structs can only use skip(from)")); + } + + Ok(self) + } +} + +#[derive(Debug, Default, FromMeta)] +pub struct ContainerSkipArguments { + pub from: Flag, + pub object_from: Flag, + pub merged_crd: Flag, + pub try_convert: Flag, +} + +/// This struct contains supported CRD arguments. +/// +/// The arguments are passed through to the `#[kube]` attribute. More details can be found in the +/// official docs: . +/// +/// Supported arguments are: +/// +/// - `group`: Set the group of the CR object, usually the domain of the company. +/// This argument is Required. +/// - `kind`: Override the kind field of the CR object. This defaults to the struct +/// name (without the 'Spec' suffix). +/// - `singular`: Set the singular name of the CR object. +/// - `plural`: Set the plural name of the CR object. +/// - `namespaced`: Indicate that this is a namespaced scoped resource rather than a +/// cluster scoped resource. +/// - `crates`: Override specific crates. +/// - `status`: Set the specified struct as the status subresource. +/// - `shortname`: Set a shortname for the CR object. This can be specified multiple +/// times. +/// - `skip`: Controls skipping parts of the generation. +#[derive(Clone, Debug, FromMeta)] +pub struct StructCrdArguments { + pub group: String, + pub kind: Option, + pub singular: Option, + pub plural: Option, + pub namespaced: Flag, + // root + pub status: Option, + // derive + // schema + // scale + // printcolumn + #[darling(multiple, rename = "shortname")] + pub shortnames: Vec, + // category + // selectable + // doc + // annotation + // label +} diff --git a/crates/stackable-versioned-macros/src/attrs/container/k8s.rs b/crates/stackable-versioned-macros/src/attrs/container/k8s.rs deleted file mode 100644 index 88ca013d4..000000000 --- a/crates/stackable-versioned-macros/src/attrs/container/k8s.rs +++ /dev/null @@ -1,165 +0,0 @@ -use darling::{FromMeta, util::Flag}; -use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; -use syn::{Path, parse_quote}; - -use crate::attrs::common::Override; - -/// This struct contains supported Kubernetes arguments. -/// -/// The arguments are passed through to the `#[kube]` attribute. More details can be found in the -/// official docs: . -/// -/// Supported arguments are: -/// -/// - `group`: Set the group of the CR object, usually the domain of the company. -/// This argument is Required. -/// - `kind`: Override the kind field of the CR object. This defaults to the struct -/// name (without the 'Spec' suffix). -/// - `singular`: Set the singular name of the CR object. -/// - `plural`: Set the plural name of the CR object. -/// - `namespaced`: Indicate that this is a namespaced scoped resource rather than a -/// cluster scoped resource. -/// - `crates`: Override specific crates. -/// - `status`: Set the specified struct as the status subresource. -/// - `shortname`: Set a shortname for the CR object. This can be specified multiple -/// times. -/// - `skip`: Controls skipping parts of the generation. -#[derive(Clone, Debug, FromMeta)] -pub struct KubernetesArguments { - pub group: String, - pub kind: Option, - pub singular: Option, - pub plural: Option, - pub namespaced: Flag, - // root - #[darling(default)] - pub crates: KubernetesCrateArguments, - pub status: Option, - // derive - // schema - // scale - // printcolumn - #[darling(multiple, rename = "shortname")] - pub shortnames: Vec, - // category - // selectable - // doc - // annotation - // label - #[darling(default)] - pub options: KubernetesConfigOptions, -} - -/// This struct contains crate overrides to be passed to `#[kube]`. -#[derive(Clone, Debug, FromMeta)] -pub struct KubernetesCrateArguments { - #[darling(default = default_kube_core)] - pub kube_core: Override, - - #[darling(default = default_kube_client)] - pub kube_client: Override, - - #[darling(default = default_k8s_openapi)] - pub k8s_openapi: Override, - - #[darling(default = default_schemars)] - pub schemars: Override, - - #[darling(default = default_serde)] - pub serde: Override, - - #[darling(default = default_serde_json)] - pub serde_json: Override, - - #[darling(default = default_versioned)] - pub versioned: Override, -} - -impl Default for KubernetesCrateArguments { - fn default() -> Self { - Self { - kube_core: default_kube_core(), - kube_client: default_kube_client(), - k8s_openapi: default_k8s_openapi(), - schemars: default_schemars(), - serde: default_serde(), - serde_json: default_serde_json(), - versioned: default_versioned(), - } - } -} - -fn default_kube_core() -> Override { - Override::Default(parse_quote! { ::kube::core }) -} - -fn default_kube_client() -> Override { - Override::Default(parse_quote! { ::kube::client }) -} - -fn default_k8s_openapi() -> Override { - Override::Default(parse_quote! { ::k8s_openapi }) -} - -fn default_schemars() -> Override { - Override::Default(parse_quote! { ::schemars }) -} - -fn default_serde() -> Override { - Override::Default(parse_quote! { ::serde }) -} - -fn default_serde_json() -> Override { - Override::Default(parse_quote! { ::serde_json }) -} - -fn default_versioned() -> Override { - Override::Default(parse_quote! { ::stackable_versioned }) -} - -impl ToTokens for KubernetesCrateArguments { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let mut crate_overrides = TokenStream::new(); - - let KubernetesCrateArguments { - kube_client: _, - k8s_openapi, - serde_json, - kube_core, - schemars, - serde, - .. - } = self; - - if let Override::Explicit(k8s_openapi) = k8s_openapi { - crate_overrides.extend(quote! { k8s_openapi = #k8s_openapi, }); - } - - if let Override::Explicit(serde_json) = serde_json { - crate_overrides.extend(quote! { serde_json = #serde_json, }); - } - - if let Override::Explicit(kube_core) = kube_core { - crate_overrides.extend(quote! { kube_core = #kube_core, }); - } - - if let Override::Explicit(schemars) = schemars { - crate_overrides.extend(quote! { schemars = #schemars, }); - } - - if let Override::Explicit(serde) = serde { - crate_overrides.extend(quote! { serde = #serde, }); - } - - if !crate_overrides.is_empty() { - tokens.extend(quote! { , crates(#crate_overrides) }); - } - } -} - -#[derive(Clone, Default, Debug, FromMeta)] -pub struct KubernetesConfigOptions { - pub experimental_conversion_tracking: Flag, - pub enable_tracing: Flag, -} diff --git a/crates/stackable-versioned-macros/src/attrs/container/mod.rs b/crates/stackable-versioned-macros/src/attrs/container/mod.rs deleted file mode 100644 index f43a06866..000000000 --- a/crates/stackable-versioned-macros/src/attrs/container/mod.rs +++ /dev/null @@ -1,72 +0,0 @@ -use darling::{Error, FromAttributes, FromMeta, Result, util::Flag}; - -use crate::attrs::{ - common::{CommonOptions, CommonRootArguments, SkipArguments}, - container::k8s::KubernetesArguments, -}; - -pub mod k8s; - -#[derive(Debug, FromMeta)] -#[darling(and_then = StandaloneContainerAttributes::validate)] -pub struct StandaloneContainerAttributes { - #[darling(rename = "k8s")] - pub kubernetes_arguments: Option, - - #[darling(flatten)] - pub common: CommonRootArguments, -} - -impl StandaloneContainerAttributes { - fn validate(self) -> Result { - if self.kubernetes_arguments.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) - } -} - -#[derive(Debug, FromMeta, Default)] -pub struct StandaloneContainerOptions { - pub allow_unsorted: Flag, - pub skip: Option, -} - -impl CommonOptions for StandaloneContainerOptions { - fn allow_unsorted(&self) -> Flag { - self.allow_unsorted - } -} - -#[derive(Debug, FromAttributes)] -#[darling( - attributes(versioned), - and_then = NestedContainerAttributes::validate -)] -pub struct NestedContainerAttributes { - #[darling(rename = "k8s")] - pub kubernetes_arguments: Option, - - #[darling(default)] - pub options: NestedContainerOptionArguments, -} - -impl NestedContainerAttributes { - fn validate(self) -> Result { - if self.kubernetes_arguments.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) - } -} - -#[derive(Debug, Default, FromMeta)] -pub struct NestedContainerOptionArguments { - pub skip: Option, -} diff --git a/crates/stackable-versioned-macros/src/attrs/mod.rs b/crates/stackable-versioned-macros/src/attrs/mod.rs index 4442ca4f6..1a484e0b7 100644 --- a/crates/stackable-versioned-macros/src/attrs/mod.rs +++ b/crates/stackable-versioned-macros/src/attrs/mod.rs @@ -1,4 +1,3 @@ -pub mod common; pub mod container; pub mod item; pub mod module; diff --git a/crates/stackable-versioned-macros/src/attrs/module.rs b/crates/stackable-versioned-macros/src/attrs/module.rs index addc1a93c..71d61d34b 100644 --- a/crates/stackable-versioned-macros/src/attrs/module.rs +++ b/crates/stackable-versioned-macros/src/attrs/module.rs @@ -1,22 +1,264 @@ -use darling::{FromMeta, util::Flag}; +use std::ops::Deref; -use crate::attrs::common::{CommonOptions, CommonRootArguments, SkipArguments}; +use darling::{ + Error, FromMeta, Result, + util::{Flag, Override as FlagOrOverride, SpannedValue}, +}; +use itertools::Itertools as _; +use k8s_version::Version; +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; +use syn::{Path, parse_quote}; #[derive(Debug, FromMeta)] +#[darling(and_then = ModuleAttributes::validate)] pub struct ModuleAttributes { - #[darling(flatten)] - pub common: CommonRootArguments, + #[darling(multiple, rename = "version")] + pub versions: SpannedValue>, + + #[darling(default)] + pub crates: CrateArguments, + + #[darling(default)] + pub options: ModuleOptions, + + #[darling(default)] + pub skip: ModuleSkipArguments, +} + +impl ModuleAttributes { + fn validate(mut self) -> Result { + let mut errors = Error::accumulator(); + + if self.versions.is_empty() { + errors.push( + Error::custom("at least one or more `version`s must be defined") + .with_span(&self.versions.span()), + ); + } + + let is_sorted = self.versions.iter().is_sorted_by_key(|v| v.name); + + // It needs to be sorted, even though the definition could be unsorted + // (if allow_unsorted is set). + self.versions.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); + + if !self.options.common.allow_unsorted.is_present() && !is_sorted { + let versions = self.versions.iter().map(|v| v.name).join(", "); + + errors.push(Error::custom(format!( + "versions must be defined in ascending order: {versions}", + ))); + } + + let duplicate_versions: Vec<_> = self + .versions + .iter() + .duplicates_by(|v| v.name) + .map(|v| v.name) + .collect(); + + if !duplicate_versions.is_empty() { + let versions = duplicate_versions.iter().join(", "); + + errors.push(Error::custom(format!( + "contains duplicate versions: {versions}", + ))); + } + + errors.finish_with(self) + } } -#[derive(Debug, FromMeta, Default)] +#[derive(Debug, Default, FromMeta)] pub struct ModuleOptions { + #[darling(flatten)] + pub common: ModuleCommonOptions, + + #[darling(default, rename = "k8s")] + pub kubernetes: KubernetesConfigOptions, +} + +#[derive(Debug, Default, FromMeta)] +pub struct ModuleCommonOptions { pub allow_unsorted: Flag, - pub skip: Option, pub preserve_module: Flag, } -impl CommonOptions for ModuleOptions { - fn allow_unsorted(&self) -> Flag { - self.allow_unsorted +#[derive(Debug, Default, FromMeta)] +pub struct ModuleSkipArguments { + pub from: Flag, + pub object_from: Flag, + pub merged_crd: Flag, + pub try_convert: Flag, +} + +/// This struct contains crate overrides to be passed to `#[kube]`. +#[derive(Clone, Debug, FromMeta)] +pub struct CrateArguments { + #[darling(default = default_kube_core)] + pub kube_core: Override, + + #[darling(default = default_kube_client)] + pub kube_client: Override, + + #[darling(default = default_k8s_openapi)] + pub k8s_openapi: Override, + + #[darling(default = default_schemars)] + pub schemars: Override, + + #[darling(default = default_serde)] + pub serde: Override, + + #[darling(default = default_serde_json)] + pub serde_json: Override, + + #[darling(default = default_serde_yaml)] + pub serde_yaml: Override, + + #[darling(default = default_versioned)] + pub versioned: Override, +} + +impl Default for CrateArguments { + fn default() -> Self { + Self { + kube_core: default_kube_core(), + kube_client: default_kube_client(), + k8s_openapi: default_k8s_openapi(), + schemars: default_schemars(), + serde: default_serde(), + serde_json: default_serde_json(), + serde_yaml: default_serde_yaml(), + versioned: default_versioned(), + } + } +} + +fn default_kube_core() -> Override { + Override::Default(parse_quote! { ::kube::core }) +} + +fn default_kube_client() -> Override { + Override::Default(parse_quote! { ::kube::client }) +} + +fn default_k8s_openapi() -> Override { + Override::Default(parse_quote! { ::k8s_openapi }) +} + +fn default_schemars() -> Override { + Override::Default(parse_quote! { ::schemars }) +} + +fn default_serde() -> Override { + Override::Default(parse_quote! { ::serde }) +} + +fn default_serde_json() -> Override { + Override::Default(parse_quote! { ::serde_json }) +} + +fn default_serde_yaml() -> Override { + Override::Default(parse_quote! { ::serde_yaml }) +} + +fn default_versioned() -> Override { + Override::Default(parse_quote! { ::stackable_versioned }) +} + +impl ToTokens for CrateArguments { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let mut crate_overrides = TokenStream::new(); + + let CrateArguments { + kube_client: _, + k8s_openapi, + serde_json, + kube_core, + schemars, + serde, + .. + } = self; + + if let Override::Explicit(k8s_openapi) = k8s_openapi { + crate_overrides.extend(quote! { k8s_openapi = #k8s_openapi, }); + } + + if let Override::Explicit(serde_json) = serde_json { + crate_overrides.extend(quote! { serde_json = #serde_json, }); + } + + if let Override::Explicit(kube_core) = kube_core { + crate_overrides.extend(quote! { kube_core = #kube_core, }); + } + + if let Override::Explicit(schemars) = schemars { + crate_overrides.extend(quote! { schemars = #schemars, }); + } + + if let Override::Explicit(serde) = serde { + crate_overrides.extend(quote! { serde = #serde, }); + } + + if !crate_overrides.is_empty() { + tokens.extend(quote! { , crates(#crate_overrides) }); + } + } +} + +#[derive(Clone, Default, Debug, FromMeta)] +pub struct KubernetesConfigOptions { + pub experimental_conversion_tracking: Flag, + pub enable_tracing: Flag, +} + +/// This struct contains supported version arguments. +/// +/// Supported arguments are: +/// +/// - `name` of the version, like `v1alpha1`. +/// - `deprecated` flag to mark that version as deprecated. +/// - `skip` option to skip generating various pieces of code. +/// - `doc` option to add version-specific documentation. +#[derive(Clone, Debug, FromMeta)] +pub struct VersionArguments { + pub deprecated: Option>, + pub skip: Option, + pub doc: Option, + pub name: Version, +} + +#[derive(Clone, Debug, FromMeta)] +pub struct VersionSkipArguments { + pub from: Flag, + pub object_from: Flag, +} + +/// Wraps a value to indicate whether it is original or has been overridden. +#[derive(Clone, Debug)] +pub enum Override { + Default(T), + Explicit(T), +} + +impl FromMeta for Override +where + T: FromMeta, +{ + fn from_meta(item: &syn::Meta) -> Result { + FromMeta::from_meta(item).map(Override::Explicit) + } +} + +impl Deref for Override { + type Target = T; + + fn deref(&self) -> &Self::Target { + match &self { + Override::Default(inner) => inner, + Override::Explicit(inner) => inner, + } } } diff --git a/crates/stackable-versioned-macros/src/codegen/container/enum.rs b/crates/stackable-versioned-macros/src/codegen/container/enum.rs index 7ca6efb98..4abbe1c4f 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/enum.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/enum.rs @@ -16,45 +16,8 @@ use crate::{ }; impl Container { - pub fn new_standalone_enum( - item_enum: ItemEnum, - attributes: StandaloneContainerAttributes, - versions: &[VersionDefinition], - ) -> Result { - let mut versioned_variants = Vec::new(); - for variant in item_enum.variants { - let mut versioned_variant = VersionedVariant::new(variant, versions)?; - versioned_variant.insert_container_versions(versions); - versioned_variants.push(versioned_variant); - } - - let options = ContainerOptions { - kubernetes_arguments: None, - skip_from: attributes - .common - .options - .skip - .is_some_and(|s| s.from.is_present()), - }; - - let idents = ContainerIdents::from(item_enum.ident, None); - - let common = CommonContainerData { - original_attributes: item_enum.attrs, - options, - idents, - }; - - Ok(Self::Enum(Enum { - generics: item_enum.generics, - variants: versioned_variants, - common, - })) - } - - // TODO (@Techassi): See what can be unified into a single 'new' function - pub fn new_enum_nested(item_enum: ItemEnum, versions: &[VersionDefinition]) -> Result { - let attributes = NestedContainerAttributes::from_attributes(&item_enum.attrs)?; + pub fn new_enum(item_enum: ItemEnum, versions: &[VersionDefinition]) -> Result { + let attributes = ContainerAttributes::from_attributes(&item_enum.attrs)?; let mut versioned_variants = Vec::new(); for variant in item_enum.variants { diff --git a/crates/stackable-versioned-macros/src/codegen/container/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/mod.rs index 8699e6738..94db6cbd8 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/mod.rs @@ -100,171 +100,62 @@ impl Container { } } -/// A versioned standalone container. -/// -/// A standalone container is a container defined outside of a versioned module. See [`Module`][1] -/// for more information about versioned modules. -/// -/// [1]: crate::codegen::module::Module -pub struct StandaloneContainer { - versions: Vec, - container: Container, - vis: Visibility, -} - -impl StandaloneContainer { - /// Creates a new versioned standalone struct. - pub fn new_struct( - item_struct: ItemStruct, - attributes: StandaloneContainerAttributes, - ) -> Result { - let versions: Vec<_> = (&attributes).into(); - let vis = item_struct.vis.clone(); - - let container = Container::new_standalone_struct(item_struct, attributes, &versions)?; - - Ok(Self { - container, - versions, - vis, - }) - } - - /// Creates a new versioned standalone enum. - pub fn new_enum( - item_enum: ItemEnum, - attributes: StandaloneContainerAttributes, - ) -> Result { - let versions: Vec<_> = (&attributes).into(); - let vis = item_enum.vis.clone(); - - let container = Container::new_standalone_enum(item_enum, attributes, &versions)?; - - Ok(Self { - container, - versions, - vis, - }) - } - - /// Generate tokens containing every piece of code required for a standalone container. - pub fn generate_tokens(&self) -> TokenStream { - let vis = &self.vis; - - let mut kubernetes_tokens = KubernetesTokens::default(); - let mut tokens = TokenStream::new(); - - let mut versions = self.versions.iter().peekable(); - - while let Some(version) = versions.next() { - let container_definition = self.container.generate_definition(version); - let module_ident = &version.idents.module; - - // NOTE (@Techassi): Using '.copied()' here does not copy or clone the data, but instead - // removes one level of indirection of the double reference '&&'. - let next_version = versions.peek().copied(); - - // Generate the From impl for upgrading the CRD. - let upgrade_from_impl = - self.container - .generate_from_impl(Direction::Upgrade, version, next_version, false); - - // Generate the From impl for downgrading the CRD. - let downgrade_from_impl = self.container.generate_from_impl( - Direction::Downgrade, - version, - next_version, - false, - ); - - // Add the #[deprecated] attribute when the version is marked as deprecated. - let deprecated_attribute = version - .deprecated - .as_ref() - .map(|note| quote! { #[deprecated = #note] }); - - // Generate Kubernetes specific code (for a particular version) which is placed outside - // of the container definition. - if let Some(items) = self.container.generate_kubernetes_version_items(version) { - kubernetes_tokens.push(items); - } - - tokens.extend(quote! { - #[automatically_derived] - #deprecated_attribute - #vis mod #module_ident { - use super::*; - #container_definition - } - - #upgrade_from_impl - #downgrade_from_impl - }); - } - - // Finally add tokens outside of the container definitions - tokens.extend(self.container.generate_kubernetes_code( - &self.versions, - &kubernetes_tokens, - vis, - false, - )); +/// A collection of container idents used for different purposes. +#[derive(Debug)] +pub struct ContainerIdents { + /// The original ident, or name, of the versioned container. + pub original: IdentString, - tokens - } + /// The ident used as a parameter. + pub parameter: IdentString, } -/// A collection of container idents used for different purposes. #[derive(Debug)] -pub struct ContainerIdents { +pub struct KubernetesIdents { /// This ident removes the 'Spec' suffix present in the definition container. /// This ident is only used in the context of Kubernetes specific code. - pub kubernetes: IdentString, + pub kind: IdentString, /// This ident uses the base Kubernetes ident to construct an appropriate ident /// for auto-generated status structs. This ident is only used in the context of /// Kubernetes specific code. - pub kubernetes_status: IdentString, + pub status: IdentString, /// This ident uses the base Kubernetes ident to construct an appropriate ident /// for auto-generated version enums. This enum is used to select the stored /// api version when merging CRDs. This ident is only used in the context of /// Kubernetes specific code. - pub kubernetes_version: IdentString, + pub version: IdentString, // TODO (@Techassi): Add comment - pub kubernetes_parameter: IdentString, - - /// The original ident, or name, of the versioned container. - pub original: IdentString, - - /// The ident used as a parameter. pub parameter: IdentString, } -impl ContainerIdents { - pub fn from(ident: Ident, kubernetes_arguments: Option<&KubernetesArguments>) -> Self { - let kubernetes = match kubernetes_arguments { - Some(args) => match &args.kind { - Some(kind) => IdentString::from(Ident::new(kind, Span::call_site())), - None => ident.as_cleaned_kubernetes_ident(), - }, +impl From for ContainerIdents { + fn from(ident: Ident) -> Self { + Self { + parameter: ident.as_parameter_ident(), + original: ident.into(), + } + } +} + +impl KubernetesIdents { + pub fn from(ident: &IdentString, arguments: &StructCrdArguments) -> Self { + let kind = match &arguments.kind { + Some(kind) => IdentString::from(Ident::new(kind, Span::call_site())), None => ident.as_cleaned_kubernetes_ident(), }; - let kubernetes_status = - IdentString::from(format_ident!("{kubernetes}StatusWithChangedValues")); - - let kubernetes_version = IdentString::from(format_ident!("{kubernetes}Version")); - let kubernetes_parameter = kubernetes.as_parameter_ident(); + let status = IdentString::from(format_ident!("{kind}StatusWithChangedValues")); + let version = IdentString::from(format_ident!("{kind}Version")); + let parameter = kind.as_parameter_ident(); Self { - parameter: ident.as_parameter_ident(), - original: ident.into(), - kubernetes_parameter, - kubernetes_version, - kubernetes_status, - kubernetes, + parameter, + version, + status, + kind, } } } diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs deleted file mode 100644 index 7023cca0c..000000000 --- a/crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs +++ /dev/null @@ -1,742 +0,0 @@ -use std::{borrow::Cow, cmp::Ordering, ops::Not as _}; - -use darling::util::IdentString; -use indoc::formatdoc; -use itertools::Itertools as _; -use proc_macro2::TokenStream; -use quote::quote; -use syn::{Visibility, parse_quote}; - -use crate::{ - attrs::container::k8s::KubernetesArguments, - codegen::{KubernetesTokens, VersionDefinition, container::r#struct::Struct}, - utils::{doc_comments::DocComments, path_to_string}, -}; - -const CONVERTED_OBJECT_COUNT_ATTRIBUTE: &str = "k8s.crd.conversion.converted_object_count"; -const DESIRED_API_VERSION_ATTRIBUTE: &str = "k8s.crd.conversion.desired_api_version"; -const API_VERSION_ATTRIBUTE: &str = "k8s.crd.conversion.api_version"; -const STEPS_ATTRIBUTE: &str = "k8s.crd.conversion.steps"; -const KIND_ATTRIBUTE: &str = "k8s.crd.conversion.kind"; - -impl Struct { - pub fn generate_kube_attribute(&self, version: &VersionDefinition) -> Option { - let kubernetes_arguments = self.common.options.kubernetes_arguments.as_ref()?; - - // Required arguments - let group = &kubernetes_arguments.group; - let version = version.inner.to_string(); - let kind = kubernetes_arguments - .kind - .as_ref() - .map_or(self.common.idents.kubernetes.to_string(), |kind| { - kind.clone() - }); - - // Optional arguments - let singular = kubernetes_arguments - .singular - .as_ref() - .map(|s| quote! { , singular = #s }); - - let plural = kubernetes_arguments - .plural - .as_ref() - .map(|p| quote! { , plural = #p }); - - let namespaced = kubernetes_arguments - .namespaced - .is_present() - .then_some(quote! { , namespaced }); - - let crates = &kubernetes_arguments.crates; - - let status = match ( - kubernetes_arguments - .options - .experimental_conversion_tracking - .is_present(), - &kubernetes_arguments.status, - ) { - (true, _) => { - let status_ident = &self.common.idents.kubernetes_status; - Some(quote! { , status = #status_ident }) - } - (_, Some(status_ident)) => Some(quote! { , status = #status_ident }), - (_, _) => None, - }; - - let shortnames: TokenStream = kubernetes_arguments - .shortnames - .iter() - .map(|s| quote! { , shortname = #s }) - .collect(); - - Some(quote! { - // The end-developer needs to derive CustomResource and JsonSchema. - // This is because we don't know if they want to use a re-exported or renamed import. - #[kube( - // These must be comma separated (except the last) as they always exist: - group = #group, version = #version, kind = #kind - // These fields are optional, and therefore the token stream must prefix each with a comma: - #singular #plural #namespaced #crates #status #shortnames - )] - }) - } - - pub fn generate_kubernetes_version_items( - &self, - version: &VersionDefinition, - ) -> Option<(TokenStream, IdentString, TokenStream, String)> { - let kubernetes_arguments = self.common.options.kubernetes_arguments.as_ref()?; - - let module_ident = &version.idents.module; - let struct_ident = &self.common.idents.kubernetes; - - let variant_data = quote! { #module_ident::#struct_ident }; - - let crd_fn = self.generate_kubernetes_crd_fn(version, kubernetes_arguments); - let variant_ident = version.idents.variant.clone(); - let variant_string = version.inner.to_string(); - - Some((crd_fn, variant_ident, variant_data, variant_string)) - } - - fn generate_kubernetes_crd_fn( - &self, - version: &VersionDefinition, - kubernetes_arguments: &KubernetesArguments, - ) -> TokenStream { - let kube_core_path = &*kubernetes_arguments.crates.kube_core; - let struct_ident = &self.common.idents.kubernetes; - let module_ident = &version.idents.module; - - quote! { - <#module_ident::#struct_ident as #kube_core_path::CustomResourceExt>::crd() - } - } - - pub fn generate_kubernetes_code( - &self, - versions: &[VersionDefinition], - tokens: &KubernetesTokens, - vis: &Visibility, - is_nested: bool, - ) -> Option { - let kubernetes_arguments = self.common.options.kubernetes_arguments.as_ref()?; - - // Get various idents needed for code generation - let variant_data_ident = &self.common.idents.kubernetes_parameter; - let version_enum_ident = &self.common.idents.kubernetes_version; - let enum_ident = &self.common.idents.kubernetes; - let enum_ident_string = enum_ident.to_string(); - - // Only add the #[automatically_derived] attribute if this impl is used outside of a - // module (in standalone mode). - let automatically_derived = is_nested.not().then(|| quote! {#[automatically_derived]}); - - // Get the crate paths - let k8s_openapi_path = &*kubernetes_arguments.crates.k8s_openapi; - let serde_json_path = &*kubernetes_arguments.crates.serde_json; - let versioned_path = &*kubernetes_arguments.crates.versioned; - let kube_core_path = &*kubernetes_arguments.crates.kube_core; - - // Get the per-version items to be able to iterate over them via quote - let variant_strings = &tokens.variant_strings; - let variant_idents = &tokens.variant_idents; - let variant_data = &tokens.variant_data; - let crd_fns = &tokens.crd_fns; - - let api_versions = variant_strings - .iter() - .map(|version| format!("{group}/{version}", group = &kubernetes_arguments.group)); - - // Generate additional Kubernetes code, this is split out to reduce the complexity in this - // function. - let status_struct = self.generate_kubernetes_status_struct(kubernetes_arguments, is_nested); - let version_enum = - self.generate_kubernetes_version_enum(kubernetes_arguments, tokens, vis, is_nested); - let convert_method = self.generate_kubernetes_conversion(versions); - - let parse_object_error = quote! { #versioned_path::ParseObjectError }; - - Some(quote! { - #automatically_derived - #vis enum #enum_ident { - #(#variant_idents(#variant_data)),* - } - - #automatically_derived - impl #enum_ident { - /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. - pub fn merged_crd( - stored_apiversion: #version_enum_ident - ) -> ::std::result::Result< - #k8s_openapi_path::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, - #kube_core_path::crd::MergeError> - { - #kube_core_path::crd::merge_crds(vec![#(#crd_fns),*], stored_apiversion.as_version_str()) - } - - #convert_method - - fn from_json_object(object_value: #serde_json_path::Value) -> ::std::result::Result { - let object_kind = object_value - .get("kind") - .ok_or_else(|| #parse_object_error::FieldMissing{ field: "kind".to_owned() })? - .as_str() - .ok_or_else(|| #parse_object_error::FieldNotStr{ field: "kind".to_owned() })?; - - // Note(@sbernauer): The kind must be checked here, because it is - // possible for the wrong object to be deserialized. - // Checking here stops us assuming the kind is correct and - // accidentally updating upgrade/downgrade information in the - // status in a later step. - if object_kind != #enum_ident_string { - return Err(#parse_object_error::UnexpectedKind{ - kind: object_kind.to_owned(), - expected: #enum_ident_string.to_owned(), - }); - } - - let api_version = object_value - .get("apiVersion") - .ok_or_else(|| #parse_object_error::FieldMissing{ field: "apiVersion".to_owned() })? - .as_str() - .ok_or_else(|| #parse_object_error::FieldNotStr{ field: "apiVersion".to_owned() })?; - - let object = match api_version { - #(#api_versions => { - let object = #serde_json_path::from_value(object_value) - .map_err(|source| #parse_object_error::Deserialize { source })?; - - Self::#variant_idents(object) - },)* - unknown_api_version => return ::std::result::Result::Err(#parse_object_error::UnknownApiVersion { - api_version: unknown_api_version.to_owned() - }), - }; - - ::std::result::Result::Ok(object) - } - - fn into_json_value(self) -> ::std::result::Result<#serde_json_path::Value, #serde_json_path::Error> { - match self { - #(Self::#variant_idents(#variant_data_ident) => Ok(#serde_json_path::to_value(#variant_data_ident)?),)* - } - } - } - - #version_enum - #status_struct - }) - } - - //////////////////// - // Merge CRD Code // - //////////////////// - - fn generate_kubernetes_version_enum( - &self, - kubernetes_arguments: &KubernetesArguments, - tokens: &KubernetesTokens, - vis: &Visibility, - is_nested: bool, - ) -> TokenStream { - let enum_ident = &self.common.idents.kubernetes_version; - - // Only add the #[automatically_derived] attribute if this impl is used outside of a - // module (in standalone mode). - let automatically_derived = is_nested.not().then(|| quote! {#[automatically_derived]}); - - let versioned_path = &*kubernetes_arguments.crates.versioned; - let unknown_desired_api_version_error = - quote! { #versioned_path::UnknownDesiredApiVersionError }; - - // Get the per-version items to be able to iterate over them via quote - let variant_strings = &tokens.variant_strings; - let variant_idents = &tokens.variant_idents; - let api_versions = variant_strings - .iter() - .map(|version| format!("{group}/{version}", group = &kubernetes_arguments.group)) - .collect::>(); - - quote! { - #automatically_derived - #[derive(Copy, Clone, Debug)] - #vis enum #enum_ident { - #(#variant_idents),* - } - - #automatically_derived - impl ::std::fmt::Display for #enum_ident { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { - // The version (without the Kubernetes group) is probably more human-readable - f.write_str(self.as_version_str()) - } - } - - #automatically_derived - impl #enum_ident { - pub fn as_version_str(&self) -> &str { - match self { - #(#enum_ident::#variant_idents => #variant_strings),* - } - } - - pub fn as_api_version_str(&self) -> &str { - match self { - #(#enum_ident::#variant_idents => #api_versions),* - } - } - - pub fn from_api_version(api_version: &str) -> Result { - match api_version { - #(#api_versions => Ok(#enum_ident::#variant_idents)),*, - _ => Err(#unknown_desired_api_version_error { - api_version: api_version.to_owned(), - }), - } - } - } - } - } - - ///////////////////////// - // CRD Conversion Code // - ///////////////////////// - - fn generate_kubernetes_status_struct( - &self, - kubernetes_arguments: &KubernetesArguments, - is_nested: bool, - ) -> Option { - kubernetes_arguments - .options - .experimental_conversion_tracking - .is_present() - .then(|| { - let status_ident = &self.common.idents.kubernetes_status; - - let versioned_crate = &*kubernetes_arguments.crates.versioned; - let schemars_crate = &*kubernetes_arguments.crates.schemars; - let serde_crate = &*kubernetes_arguments.crates.serde; - - // Only add the #[automatically_derived] attribute if this impl is used outside of a - // module (in standalone mode). - let automatically_derived = - is_nested.not().then(|| quote! {#[automatically_derived]}); - - // TODO (@Techassi): Validate that users don't specify the status we generate - let status = kubernetes_arguments.status.as_ref().map(|status| { - quote! { - #[serde(flatten)] - pub status: #status, - } - }); - - quote! { - #automatically_derived - #[derive( - ::core::clone::Clone, - ::core::fmt::Debug, - #serde_crate::Deserialize, - #serde_crate::Serialize, - #schemars_crate::JsonSchema - )] - #[serde(rename_all = "camelCase")] - pub struct #status_ident { - pub changed_values: #versioned_crate::ChangedValues, - - #status - } - } - }) - } - - fn generate_kubernetes_conversion( - &self, - versions: &[VersionDefinition], - ) -> Option { - let kubernetes_arguments = self.common.options.kubernetes_arguments.as_ref()?; - - let struct_ident = &self.common.idents.kubernetes; - let version_enum_ident = &self.common.idents.kubernetes_version; - - let kube_client_path = &*kubernetes_arguments.crates.kube_client; - let serde_json_path = &*kubernetes_arguments.crates.serde_json; - let versioned_path = &*kubernetes_arguments.crates.versioned; - let kube_core_path = &*kubernetes_arguments.crates.kube_core; - - let convert_object_error = quote! { #versioned_path::ConvertObjectError }; - - // Generate conversion paths and the match arms for these paths - let conversion_match_arms = - self.generate_kubernetes_conversion_match_arms(versions, kubernetes_arguments); - - // TODO (@Techassi): Make this a feature, drop the option from the macro arguments - // Generate tracing attributes and events if tracing is enabled - let TracingTokens { - successful_conversion_response_event, - convert_objects_instrumentation, - invalid_conversion_review_event, - try_convert_instrumentation, - } = self.generate_kubernetes_conversion_tracing(kubernetes_arguments); - - // Generate doc comments - let conversion_review_reference = - path_to_string(&parse_quote! { #kube_core_path::conversion::ConversionReview }); - - let docs = formatdoc! {" - Tries to convert a list of objects of kind [`{struct_ident}`] to the desired API version - specified in the [`ConversionReview`][cr]. - - The returned [`ConversionReview`][cr] either indicates a success or a failure, which - is handed back to the Kubernetes API server. - - [cr]: {conversion_review_reference}" - } - .into_doc_comments(); - - Some(quote! { - #(#[doc = #docs])* - #try_convert_instrumentation - pub fn try_convert(review: #kube_core_path::conversion::ConversionReview) - -> #kube_core_path::conversion::ConversionReview - { - // First, turn the review into a conversion request - let request = match #kube_core_path::conversion::ConversionRequest::from_review(review) { - ::std::result::Result::Ok(request) => request, - ::std::result::Result::Err(err) => { - #invalid_conversion_review_event - - return #kube_core_path::conversion::ConversionResponse::invalid( - #kube_client_path::Status { - status: Some(#kube_core_path::response::StatusSummary::Failure), - message: err.to_string(), - reason: err.to_string(), - details: None, - code: 400, - } - ).into_review() - } - }; - - // Convert all objects into the desired version - let response = match Self::convert_objects(request.objects, &request.desired_api_version) { - ::std::result::Result::Ok(converted_objects) => { - #successful_conversion_response_event - - // We construct the response from the ground up as the helper functions - // don't provide any benefit over manually doing it. Constructing a - // ConversionResponse via for_request is not possible due to a partial move - // of request.objects. The function internally doesn't even use the list of - // objects. The success function on ConversionResponse basically only sets - // the result to success and the converted objects to the provided list. - // The below code does the same thing. - #kube_core_path::conversion::ConversionResponse { - result: #kube_client_path::Status::success(), - types: request.types, - uid: request.uid, - converted_objects, - } - }, - ::std::result::Result::Err(err) => { - let code = err.http_status_code(); - let message = err.join_errors(); - - #kube_core_path::conversion::ConversionResponse { - result: #kube_client_path::Status { - status: Some(#kube_core_path::response::StatusSummary::Failure), - message: message.clone(), - reason: message, - details: None, - code, - }, - types: request.types, - uid: request.uid, - converted_objects: vec![], - } - }, - }; - - response.into_review() - } - - #convert_objects_instrumentation - fn convert_objects( - objects: ::std::vec::Vec<#serde_json_path::Value>, - desired_api_version: &str, - ) - -> ::std::result::Result<::std::vec::Vec<#serde_json_path::Value>, #convert_object_error> - { - let desired_api_version = #version_enum_ident::from_api_version(desired_api_version) - .map_err(|source| #convert_object_error::ParseDesiredApiVersion { source })?; - - let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); - - for object in objects { - // This clone is required because in the noop case we move the object into - // the converted objects vec. - let current_object = Self::from_json_object(object.clone()) - .map_err(|source| #convert_object_error::Parse { source })?; - - match (current_object, desired_api_version) { - #(#conversion_match_arms,)* - // In case the desired version matches the current object api version, we - // don't need to do anything. - // - // NOTE (@Techassi): I'm curious if this will ever happen? In theory the K8s - // apiserver should never send such a conversion review. - // - // Note(@sbernauer): I would prefer to explicitly list the remaining no-op - // cases, so the compiler ensures we did not miss a conversion - // // let version_idents = versions.iter().map(|v| &v.idents.variant); - // #( - // (Self::#version_idents(_), #version_enum_ident::#version_idents) - // )|* => converted_objects.push(object) - _ => converted_objects.push(object), - } - } - - ::std::result::Result::Ok(converted_objects) - } - }) - } - - fn generate_kubernetes_conversion_match_arms( - &self, - versions: &[VersionDefinition], - kubernetes_arguments: &KubernetesArguments, - ) -> Vec { - let group = &kubernetes_arguments.group; - let variant_data_ident = &self.common.idents.kubernetes_parameter; - let struct_ident = &self.common.idents.kubernetes; - let version_enum_ident = &self.common.idents.kubernetes_version; - let spec_ident = &self.common.idents.original; - - let versioned_path = &*kubernetes_arguments.crates.versioned; - let convert_object_error = quote! { #versioned_path::ConvertObjectError }; - - let conversion_paths = conversion_paths(versions); - - conversion_paths - .iter() - .map(|(start, path)| { - let current_object_version_ident = &start.idents.variant; - let current_object_version_string = &start.inner.to_string(); - - let desired_object_version = path.last().expect("the path always contains at least one element"); - let desired_object_api_version_string = format!( - "{group}/{desired_object_version}", - desired_object_version = desired_object_version.inner - ); - let desired_object_variant_ident = &desired_object_version.idents.variant; - let desired_object_module_ident = &desired_object_version.idents.module; - - let conversions = path.iter().enumerate().map(|(i, v)| { - let module_ident = &v.idents.module; - - if i == 0 { - quote! { - let converted: #module_ident::#spec_ident = #variant_data_ident.spec.into(); - } - } else { - quote! { - let converted: #module_ident::#spec_ident = converted.into(); - } - } - }); - - let kind = self.common.idents.kubernetes.to_string(); - let steps = path.len(); - - let convert_object_trace = kubernetes_arguments.options.enable_tracing.is_present().then(|| quote! { - ::tracing::trace!( - #DESIRED_API_VERSION_ATTRIBUTE = #desired_object_api_version_string, - #API_VERSION_ATTRIBUTE = #current_object_version_string, - #STEPS_ATTRIBUTE = #steps, - #KIND_ATTRIBUTE = #kind, - "Successfully converted object" - ); - }); - - // Carry over the status field if the user set a status subresource - let status_field = kubernetes_arguments.status - .is_some() - .then(|| quote! { status: #variant_data_ident.status, }); - - quote! { - (Self::#current_object_version_ident(#variant_data_ident), #version_enum_ident::#desired_object_variant_ident) => { - #(#conversions)* - - let desired_object = Self::#desired_object_variant_ident(#desired_object_module_ident::#struct_ident { - metadata: #variant_data_ident.metadata, - #status_field - spec: converted, - }); - - let desired_object = desired_object.into_json_value() - .map_err(|source| #convert_object_error::Serialize { source })?; - - #convert_object_trace - - converted_objects.push(desired_object); - } - } - }) - .collect() - } - - fn generate_kubernetes_conversion_tracing( - &self, - kubernetes_arguments: &KubernetesArguments, - ) -> TracingTokens { - if kubernetes_arguments.options.enable_tracing.is_present() { - // TODO (@Techassi): Make tracing path configurable. Currently not possible, needs - // upstream change - let kind = self.common.idents.kubernetes.to_string(); - - let successful_conversion_response_event = Some(quote! { - ::tracing::debug!( - #CONVERTED_OBJECT_COUNT_ATTRIBUTE = converted_objects.len(), - #KIND_ATTRIBUTE = #kind, - "Successfully converted objects" - ); - }); - - let convert_objects_instrumentation = Some(quote! { - #[::tracing::instrument( - skip_all, - err - )] - }); - - let invalid_conversion_review_event = Some(quote! { - ::tracing::warn!(?err, "received invalid conversion review"); - }); - - // NOTE (@Techassi): We sadly cannot use the constants here, because - // the fields only accept idents, which strings are not. - let try_convert_instrumentation = Some(quote! { - #[::tracing::instrument( - skip_all, - fields( - k8s.crd.conversion.api_version = review.types.api_version, - k8s.crd.kind = review.types.kind, - ) - )] - }); - - TracingTokens { - successful_conversion_response_event, - convert_objects_instrumentation, - invalid_conversion_review_event, - try_convert_instrumentation, - } - } else { - TracingTokens::default() - } - } -} - -#[derive(Debug, Default)] -struct TracingTokens { - successful_conversion_response_event: Option, - convert_objects_instrumentation: Option, - invalid_conversion_review_event: Option, - try_convert_instrumentation: Option, -} - -fn conversion_paths(elements: &[T]) -> Vec<(&T, Cow<'_, [T]>)> -where - T: Clone + Ord, -{ - let mut chain = Vec::new(); - - // First, create all 2-permutations of the provided list of elements. It is important - // we select permutations instead of combinations because the order of elements matter. - // A quick example of what the iterator adaptor produces: A list with three elements - // 'v1alpha1', 'v1beta1', and 'v1' will produce six (3! / (3 - 2)!) permutations: - // - // - v1alpha1 -> v1beta1 - // - v1alpha1 -> v1 - // - v1beta1 -> v1 - // - v1beta1 -> v1alpha1 - // - v1 -> v1alpha1 - // - v1 -> v1beta1 - - for pair in elements.iter().permutations(2) { - let start = pair[0]; - let end = pair[1]; - - // Next, we select the positions of the start and end element in the original - // slice. These indices are used to construct the conversion path, which contains - // elements between start (excluding) and the end (including). These elements - // describe the steps needed to go from the start to the end (upgrade or downgrade - // depending on the direction). - if let (Some(start_index), Some(end_index)) = ( - elements.iter().position(|v| v == start), - elements.iter().position(|v| v == end), - ) { - let path = match start_index.cmp(&end_index) { - Ordering::Less => { - // If the start index is smaller than the end index (upgrade), we can return - // a slice pointing directly into the original slice. That's why Cow::Borrowed - // can be used here. - Cow::Borrowed(&elements[start_index + 1..=end_index]) - } - Ordering::Greater => { - // If the start index is bigger than the end index (downgrade), we need to reverse - // the elements. With a slice, this is only possible to do in place, which is not - // what we want in this case. Instead, the data is reversed and cloned and collected - // into a Vec and Cow::Owned is used. - let path = elements[end_index..start_index] - .iter() - .rev() - .cloned() - .collect(); - Cow::Owned(path) - } - Ordering::Equal => unreachable!( - "start and end index cannot be the same due to selecting permutations" - ), - }; - - chain.push((start, path)); - } - } - - chain -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn the_path_is_the_goal() { - let paths = conversion_paths(&["v1alpha1", "v1alpha2", "v1beta1", "v1"]); - assert_eq!(paths.len(), 12); - - let expected = vec![ - ("v1alpha1", vec!["v1alpha2"]), - ("v1alpha1", vec!["v1alpha2", "v1beta1"]), - ("v1alpha1", vec!["v1alpha2", "v1beta1", "v1"]), - ("v1alpha2", vec!["v1alpha1"]), - ("v1alpha2", vec!["v1beta1"]), - ("v1alpha2", vec!["v1beta1", "v1"]), - ("v1beta1", vec!["v1alpha2", "v1alpha1"]), - ("v1beta1", vec!["v1alpha2"]), - ("v1beta1", vec!["v1"]), - ("v1", vec!["v1beta1", "v1alpha2", "v1alpha1"]), - ("v1", vec!["v1beta1", "v1alpha2"]), - ("v1", vec!["v1beta1"]), - ]; - - for (result, expected) in paths.iter().zip(expected) { - assert_eq!(*result.0, expected.0); - assert_eq!(result.1.to_vec(), expected.1); - } - } -} diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs index 1bdbb25ad..55f01f3ab 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs @@ -18,59 +18,8 @@ use crate::{ mod k8s; impl Container { - pub fn new_standalone_struct( - item_struct: ItemStruct, - attributes: StandaloneContainerAttributes, - versions: &[VersionDefinition], - ) -> Result { - // NOTE (@Techassi): Should we check if the fields are named here? - let mut versioned_fields = Vec::new(); - - for field in item_struct.fields { - let mut versioned_field = VersionedField::new(field, versions)?; - versioned_field.insert_container_versions(versions); - versioned_fields.push(versioned_field); - } - - let kubernetes_arguments = attributes.kubernetes_arguments; - let idents = ContainerIdents::from(item_struct.ident, kubernetes_arguments.as_ref()); - - // Validate K8s specific requirements - // Ensure that the struct name includes the 'Spec' suffix. - if kubernetes_arguments.is_some() && !idents.original.as_str().ends_with("Spec") { - return Err(Error::custom( - "struct name needs to include the `Spec` suffix if Kubernetes features are enabled via `#[versioned(k8s())]`" - ).with_span(&idents.original.span())); - } - - let options = ContainerOptions { - skip_from: attributes - .common - .options - .skip - .is_some_and(|s| s.from.is_present()), - kubernetes_arguments, - }; - - let common = CommonContainerData { - original_attributes: item_struct.attrs, - options, - idents, - }; - - Ok(Self::Struct(Struct { - generics: item_struct.generics, - fields: versioned_fields, - common, - })) - } - - // TODO (@Techassi): See what can be unified into a single 'new' function - pub fn new_struct_nested( - item_struct: ItemStruct, - versions: &[VersionDefinition], - ) -> Result { - let attributes = NestedContainerAttributes::from_attributes(&item_struct.attrs)?; + pub fn new_struct(item_struct: ItemStruct, versions: &[VersionDefinition]) -> Result { + let attributes = ContainerAttributes::from_attributes(&item_struct.attrs)?; let mut versioned_fields = Vec::new(); for field in item_struct.fields { @@ -128,10 +77,17 @@ pub struct Struct { /// Common container data which is shared between structs and enums. pub common: CommonContainerData, + pub kubernetes_data: Option, + /// Generic types of the struct pub generics: Generics, } +pub struct KubernetesData { + pub kubernetes_arguments: StructCrdArguments, + pub kubernetes_idents: KubernetesIdents, +} + // Common token generation impl Struct { /// Generates code for the struct definition. diff --git a/crates/stackable-versioned-macros/src/codegen/mod.rs b/crates/stackable-versioned-macros/src/codegen/mod.rs index 6c39b2e51..4dd91163c 100644 --- a/crates/stackable-versioned-macros/src/codegen/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/mod.rs @@ -4,7 +4,7 @@ use proc_macro2::TokenStream; use syn::{Path, Type}; use crate::{ - attrs::{container::StandaloneContainerAttributes, module::ModuleAttributes}, + attrs::module::ModuleAttributes, utils::{VersionExt, doc_comments::DocComments}, }; @@ -43,35 +43,9 @@ impl Ord for VersionDefinition { } } -// NOTE (@Techassi): Can we maybe unify these two impls? -impl From<&StandaloneContainerAttributes> for Vec { - fn from(attributes: &StandaloneContainerAttributes) -> Self { - attributes - .common - .versions - .iter() - .map(|v| VersionDefinition { - skip_from: v.skip.as_ref().is_some_and(|s| s.from.is_present()), - idents: VersionIdents { - module: v.name.as_module_ident(), - variant: v.name.as_variant_ident(), - }, - deprecated: v.deprecated.as_ref().map(|r#override| { - r#override - .clone() - .unwrap_or(format!("Version {version} is deprecated", version = v.name)) - }), - docs: v.doc.as_deref().into_doc_comments(), - inner: v.name, - }) - .collect() - } -} - impl From<&ModuleAttributes> for Vec { fn from(attributes: &ModuleAttributes) -> Self { attributes - .common .versions .iter() .map(|v| VersionDefinition { diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs index a028be25e..2474926e9 100644 --- a/crates/stackable-versioned-macros/src/lib.rs +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -711,290 +711,285 @@ mod utils; /// } /// ``` /// -#[cfg_attr( - feature = "k8s", - doc = r#" -# Kubernetes-specific Features - -This macro also offers support for Kubernetes-specific versioning, -especially for CustomResourceDefinitions (CRDs). These features are -completely opt-in. You need to enable the `k8s` feature (which enables -optional dependencies) and use the `k8s()` parameter in the macro. - -You need to derive both [`kube::CustomResource`] and [`schemars::JsonSchema`][1]. - -## Simple Versioning - -``` -# use stackable_versioned_macros::versioned; -use kube::CustomResource; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -#[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1"), - version(name = "v1"), - k8s(group = "example.com") -)] -#[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] -pub struct FooSpec { - #[versioned( - added(since = "v1beta1"), - changed( - since = "v1", - from_name = "prev_bar", - from_type = "u16", - downgrade_with = usize_to_u16 - ) - )] - bar: usize, - baz: bool, -} - -fn usize_to_u16(input: usize) -> u16 { - input.try_into().unwrap() -} -# fn main() {} -``` - -## Versioning Items in a Module - -Versioning multiple CRD related structs via a module is supported and common -rules from [above](#versioning-items-in-a-module) apply here as well. It should -however be noted, that specifying Kubernetes specific arguments is done on the -container level instead of on the module level, which is detailed in the -following example: - -``` -# use stackable_versioned_macros::versioned; -# use kube::CustomResource; -# use schemars::JsonSchema; -# use serde::{Deserialize, Serialize}; -#[versioned( - version(name = "v1alpha1"), - version(name = "v1") -)] -mod versioned { - #[versioned(k8s(group = "foo.example.org"))] - #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] - struct FooSpec { - bar: usize, - } - - #[versioned(k8s(group = "bar.example.org"))] - #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] - struct BarSpec { - baz: String, - } -} -# fn main() {} -``` - -
-Expand Generated Code - -```ignore -mod v1alpha1 { - use super::*; - #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] - #[kube( - group = "foo.example.org", - version = "v1alpha1", - kind = "Foo" - )] - pub struct FooSpec { - pub bar: usize, - } - - #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] - #[kube( - group = "bar.example.org", - version = "v1alpha1", - kind = "Bar" - )] - pub struct BarSpec { - pub bar: usize, - } -} - -mod v1 { - use super::*; - #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] - #[kube( - group = "foo.example.org", - version = "v1", - kind = "Foo" - )] - pub struct FooSpec { - pub bar: usize, - } - - #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] - #[kube( - group = "bar.example.org", - version = "v1", - kind = "Bar" - )] - pub struct BarSpec { - pub bar: usize, - } -} -``` -
- -It is possible to include structs and enums which are not CRDs. They are instead -versioned as expected (without adding the `#[kube]` derive macro and generating -code to merge CRD versions). - -## Arguments - -Currently, the following Kubernetes (kube) specific arguments are supported - -### `#[versioned(k8s(group = "..."))]` - -**Required.** Set the group of the CRD, usually the domain of the company, like -`example.com`. - -### `#[versioned(k8s(kind = "..."))]` - -Override the kind field of the CRD. This defaults to the struct name -(without the `Spec` suffix). Overriding this value will also influence the names -of other generated items, like the status struct (if used) or the version enum. - -### `#[versioned(k8s(singular = "..."))]` - -Set the singular name. Defaults to lowercased `kind` value. - -### `#[versioned(k8s(plural = "..."))]` - -Set the plural name. Defaults to inferring from singular. - -### `#[versioned(k8s(namespaced))]` - -Indicate that this is a namespaced scoped resource rather than a cluster scoped -resource. - -### `#[versioned(k8s(crates(...)))]` - -Override the import path of specific crates. The following code block depicts -supported overrides and their default values. - -```ignore -#[versioned(k8s(crates( - kube_core = ::kube::core, - kube_client = ::kube::client, - k8s_openapi = ::k8s_openapi, - schemars = ::schemars, - serde = ::serde, - serde_json = ::serde_json, - versioned = ::stackable_versioned, -)))] -pub struct Foo {} -``` - -### `#[versioned(k8s(status = "..."))]` - -Set the specified struct as the status subresource. If conversion tracking is -enabled, this struct will be automatically merged into the generated tracking -status struct. - -### `#[versioned(k8s(shortname = "..."))]` - -Set a shortname. This can be specified multiple times. - -### `#[versioned(k8s(options(...)))]` - -```ignore -#[versioned(k8s(options( - // Highly experimental conversion tracking. Opting into this feature will - // introduce frequent breaking changes. - experimental_conversion_tracking, - - // Enables instrumentation and log events via the tracing crate. - enable_tracing, -)))] -pub struct Foo {} -``` - -## Merge CRDs - -The generated `merged_crd` method is a wrapper around [kube's `merge_crds`][2] -function. It automatically calls the `crd` methods of the CRD in all of its -versions and additionally provides a strongly typed selector for the stored -API version. - -``` -# use stackable_versioned_macros::versioned; -# use kube::CustomResource; -# use schemars::JsonSchema; -# use serde::{Deserialize, Serialize}; -#[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1"), - k8s(group = "example.com") -)] -#[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] -pub struct FooSpec { - #[versioned(added(since = "v1beta1"))] - bar: usize, - baz: bool, -} - -# fn main() { -let merged_crd = Foo::merged_crd(FooVersion::V1Beta1).unwrap(); -println!("{yaml}", yaml = serde_yaml::to_string(&merged_crd).unwrap()); -# } -``` - -## Convert CRDs - -The conversion of CRDs is tightly integrated with ConversionReviews, the payload -which a conversion webhook receives from the K8s apiserver. Naturally, the -`try_convert` function takes in ConversionReview as a parameter and also returns -a ConversionReview indicating success or failure. - -```ignore -# use stackable_versioned_macros::versioned; -# use kube::CustomResource; -# use schemars::JsonSchema; -# use serde::{Deserialize, Serialize}; -#[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1"), - k8s(group = "example.com") -)] -#[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] -pub struct FooSpec { - #[versioned(added(since = "v1beta1"))] - bar: usize, - baz: bool, -} - -# fn main() { -let conversion_review = Foo::try_convert(conversion_review); -# } -``` - -## OpenTelemetry Semantic Conventions - -If tracing is enabled, various traces and events are emitted. The fields of these -signals follow the general rules of OpenTelemetry semantic conventions. There are -currently no agreed-upon semantic conventions for CRD conversions. In the meantime -these fields are used: - -| Field | Type (Example) | Description | -| :---- | :------------- | :---------- | -| `k8s.crd.conversion.converted_object_count` | usize (6) | The number of successfully converted objects sent back in a conversion review | -| `k8s.crd.conversion.desired_api_version` | String (v1alpha1) | The desired api version received via a conversion review | -| `k8s.crd.conversion.api_version` | String (v1beta1) | The current api version of an object received via a conversion review | -| `k8s.crd.conversion.steps` | usize (2) | The number of steps required to convert a single object from the current to the desired version | -| `k8s.crd.conversion.kind` | String (Foo) | The kind of the CRD | - -[1]: https://docs.rs/schemars/latest/schemars/derive.JsonSchema.html -[2]: https://docs.rs/kube/latest/kube/core/crd/fn.merge_crds.html -"# -)] +/// # Kubernetes-specific Features +/// +/// This macro also offers support for Kubernetes-specific versioning, +/// especially for CustomResourceDefinitions (CRDs). These features are +/// completely opt-in. You need to enable the `k8s` feature (which enables +/// optional dependencies) and use the `k8s()` parameter in the macro. +/// +/// You need to derive both [`kube::CustomResource`] and [`schemars::JsonSchema`][1]. +/// +/// ## Simple Versioning +/// +/// ``` +/// # use stackable_versioned_macros::versioned; +/// use kube::CustomResource; +/// use schemars::JsonSchema; +/// use serde::{Deserialize, Serialize}; +/// +/// #[versioned( +/// version(name = "v1alpha1"), +/// version(name = "v1beta1"), +/// version(name = "v1"), +/// k8s(group = "example.com") +/// )] +/// #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] +/// pub struct FooSpec { +/// #[versioned( +/// added(since = "v1beta1"), +/// changed( +/// since = "v1", +/// from_name = "prev_bar", +/// from_type = "u16", +/// downgrade_with = usize_to_u16 +/// ) +/// )] +/// bar: usize, +/// baz: bool, +/// } +/// +/// fn usize_to_u16(input: usize) -> u16 { +/// input.try_into().unwrap() +/// } +/// # fn main() {} +/// ``` +/// +/// ## Versioning Items in a Module +/// +/// Versioning multiple CRD related structs via a module is supported and common +/// rules from [above](#versioning-items-in-a-module) apply here as well. It should +/// however be noted, that specifying Kubernetes specific arguments is done on the +/// container level instead of on the module level, which is detailed in the +/// following example: +/// +/// ``` +/// # use stackable_versioned_macros::versioned; +/// # use kube::CustomResource; +/// # use schemars::JsonSchema; +/// # use serde::{Deserialize, Serialize}; +/// #[versioned( +/// version(name = "v1alpha1"), +/// version(name = "v1") +/// )] +/// mod versioned { +/// #[versioned(k8s(group = "foo.example.org"))] +/// #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] +/// struct FooSpec { +/// bar: usize, +/// } +/// +/// #[versioned(k8s(group = "bar.example.org"))] +/// #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] +/// struct BarSpec { +/// baz: String, +/// } +/// } +/// # fn main() {} +/// ``` +/// +///
+/// Expand Generated Code +/// +/// ```ignore +/// mod v1alpha1 { +/// use super::*; +/// #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] +/// #[kube( +/// group = "foo.example.org", +/// version = "v1alpha1", +/// kind = "Foo" +/// )] +/// pub struct FooSpec { +/// pub bar: usize, +/// } +/// +/// #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] +/// #[kube( +/// group = "bar.example.org", +/// version = "v1alpha1", +/// kind = "Bar" +/// )] +/// pub struct BarSpec { +/// pub bar: usize, +/// } +/// } +/// +/// mod v1 { +/// use super::*; +/// #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] +/// #[kube( +/// group = "foo.example.org", +/// version = "v1", +/// kind = "Foo" +/// )] +/// pub struct FooSpec { +/// pub bar: usize, +/// } +/// +/// #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] +/// #[kube( +/// group = "bar.example.org", +/// version = "v1", +/// kind = "Bar" +/// )] +/// pub struct BarSpec { +/// pub bar: usize, +/// } +/// } +/// ``` +///
+/// +/// It is possible to include structs and enums which are not CRDs. They are instead +/// versioned as expected (without adding the `#[kube]` derive macro and generating +/// code to merge CRD versions). +/// +/// ## Arguments +/// +/// Currently, the following Kubernetes (kube) specific arguments are supported +/// +/// ### `#[versioned(k8s(group = "..."))]` +/// +/// **Required.** Set the group of the CRD, usually the domain of the company, like +/// `example.com`. +/// +/// ### `#[versioned(k8s(kind = "..."))]` +/// +/// Override the kind field of the CRD. This defaults to the struct name +/// (without the `Spec` suffix). Overriding this value will also influence the names +/// of other generated items, like the status struct (if used) or the version enum. +/// +/// ### `#[versioned(k8s(singular = "..."))]` +/// +/// Set the singular name. Defaults to lowercased `kind` value. +/// +/// ### `#[versioned(k8s(plural = "..."))]` +/// +/// Set the plural name. Defaults to inferring from singular. +/// +/// ### `#[versioned(k8s(namespaced))]` +/// +/// Indicate that this is a namespaced scoped resource rather than a cluster scoped +/// resource. +/// +/// ### `#[versioned(k8s(crates(...)))]` +/// +/// Override the import path of specific crates. The following code block depicts +/// supported overrides and their default values. +/// +/// ```ignore +/// #[versioned(k8s(crates( +/// kube_core = ::kube::core, +/// kube_client = ::kube::client, +/// k8s_openapi = ::k8s_openapi, +/// schemars = ::schemars, +/// serde = ::serde, +/// serde_json = ::serde_json, +/// versioned = ::stackable_versioned, +/// )))] +/// pub struct Foo {} +/// ``` +/// +/// ### `#[versioned(k8s(status = "..."))]` +/// +/// Set the specified struct as the status subresource. If conversion tracking is +/// enabled, this struct will be automatically merged into the generated tracking +/// status struct. +/// +/// ### `#[versioned(k8s(shortname = "..."))]` +/// +/// Set a shortname. This can be specified multiple times. +/// +/// ### `#[versioned(k8s(options(...)))]` +/// +/// ```ignore +/// #[versioned(k8s(options( +/// // Highly experimental conversion tracking. Opting into this feature will +/// // introduce frequent breaking changes. +/// experimental_conversion_tracking, +/// +/// // Enables instrumentation and log events via the tracing crate. +/// enable_tracing, +/// )))] +/// pub struct Foo {} +/// ``` +/// +/// ## Merge CRDs +/// +/// The generated `merged_crd` method is a wrapper around [kube's `merge_crds`][2] +/// function. It automatically calls the `crd` methods of the CRD in all of its +/// versions and additionally provides a strongly typed selector for the stored +/// API version. +/// +/// ``` +/// # use stackable_versioned_macros::versioned; +/// # use kube::CustomResource; +/// # use schemars::JsonSchema; +/// # use serde::{Deserialize, Serialize}; +/// #[versioned( +/// version(name = "v1alpha1"), +/// version(name = "v1beta1"), +/// k8s(group = "example.com") +/// )] +/// #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] +/// pub struct FooSpec { +/// #[versioned(added(since = "v1beta1"))] +/// bar: usize, +/// baz: bool, +/// } +/// +/// # fn main() { +/// let merged_crd = Foo::merged_crd(FooVersion::V1Beta1).unwrap(); +/// println!("{yaml}", yaml = serde_yaml::to_string(&merged_crd).unwrap()); +/// # } +/// ``` +/// +/// ## Convert CRDs +/// +/// The conversion of CRDs is tightly integrated with ConversionReviews, the payload +/// which a conversion webhook receives from the K8s apiserver. Naturally, the +/// `try_convert` function takes in ConversionReview as a parameter and also returns +/// a ConversionReview indicating success or failure. +/// +/// ```ignore +/// # use stackable_versioned_macros::versioned; +/// # use kube::CustomResource; +/// # use schemars::JsonSchema; +/// # use serde::{Deserialize, Serialize}; +/// #[versioned( +/// version(name = "v1alpha1"), +/// version(name = "v1beta1"), +/// k8s(group = "example.com") +/// )] +/// #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] +/// pub struct FooSpec { +/// #[versioned(added(since = "v1beta1"))] +/// bar: usize, +/// baz: bool, +/// } +/// +/// # fn main() { +/// let conversion_review = Foo::try_convert(conversion_review); +/// # } +/// ``` +/// +/// ## OpenTelemetry Semantic Conventions +/// +/// If tracing is enabled, various traces and events are emitted. The fields of these +/// signals follow the general rules of OpenTelemetry semantic conventions. There are +/// currently no agreed-upon semantic conventions for CRD conversions. In the meantime +/// these fields are used: +/// +/// | Field | Type (Example) | Description | +/// | :---- | :------------- | :---------- | +/// | `k8s.crd.conversion.converted_object_count` | usize (6) | The number of successfully converted objects sent back in a conversion review | +/// | `k8s.crd.conversion.desired_api_version` | String (v1alpha1) | The desired api version received via a conversion review | +/// | `k8s.crd.conversion.api_version` | String (v1beta1) | The current api version of an object received via a conversion review | +/// | `k8s.crd.conversion.steps` | usize (2) | The number of steps required to convert a single object from the current to the desired version | +/// | `k8s.crd.conversion.kind` | String (Foo) | The kind of the CRD | +/// +/// [1]: https://docs.rs/schemars/latest/schemars/derive.JsonSchema.html +/// [2]: https://docs.rs/kube/latest/kube/core/crd/fn.merge_crds.html #[proc_macro_attribute] pub fn versioned(attrs: TokenStream, input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as Item); diff --git a/crates/stackable-versioned-macros/tests/trybuild.rs b/crates/stackable-versioned-macros/tests/trybuild.rs index ff26900a7..a8e6388ff 100644 --- a/crates/stackable-versioned-macros/tests/trybuild.rs +++ b/crates/stackable-versioned-macros/tests/trybuild.rs @@ -21,9 +21,9 @@ mod inputs { // mod attribute_enum; // mod attribute_struct; // mod basic_struct; - // mod downgrade_with; // mod deprecate_enum; // mod deprecate_struct; + // mod downgrade_with; // mod enum_data_simple; // mod generics_defaults; // mod generics_module; @@ -46,7 +46,6 @@ mod inputs { } } - #[cfg(feature = "k8s")] mod k8s { mod pass { // mod basic; @@ -72,7 +71,6 @@ fn default() { t.pass("tests/inputs/default/pass/*.rs"); } -#[cfg(feature = "k8s")] #[test] fn k8s() { let t = trybuild::TestCases::new(); diff --git a/crates/stackable-versioned/Cargo.toml b/crates/stackable-versioned/Cargo.toml index 1db42accd..31638486c 100644 --- a/crates/stackable-versioned/Cargo.toml +++ b/crates/stackable-versioned/Cargo.toml @@ -10,27 +10,15 @@ repository.workspace = true [package.metadata."docs.rs"] all-features = true -[features] -full = ["k8s"] -k8s = [ - "stackable-versioned-macros/k8s", # Forward the k8s feature to the underlying macro crate - "dep:k8s-version", - "dep:schemars", - "dep:serde_json", - "dep:serde_yaml", - "dep:serde", - "dep:snafu", -] - [dependencies] -k8s-version = { path = "../k8s-version", features = ["serde"], optional = true } stackable-versioned-macros = { path = "../stackable-versioned-macros" } +k8s-version = { path = "../k8s-version", features = ["serde"] } -schemars = { workspace = true, optional = true } -serde = { workspace = true, optional = true } -serde_json = { workspace = true, optional = true } -serde_yaml = { workspace = true, optional = true } -snafu = { workspace = true, optional = true } +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_yaml.workspace = true +snafu.workspace = true [dev-dependencies] insta.workspace = true diff --git a/crates/stackable-versioned/src/k8s.rs b/crates/stackable-versioned/src/k8s.rs deleted file mode 100644 index f55182fa8..000000000 --- a/crates/stackable-versioned/src/k8s.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::collections::HashMap; - -use k8s_version::Version; -use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec}; -use snafu::{ErrorCompat, Snafu}; - -// NOTE (@Techassi): This struct represents a rough first draft of how tracking values across -// CRD versions can be achieved. It is currently untested and unproven and might change down the -// line. Currently, this struct is only generated by the macro but not actually used by any other -// code. The tracking itself will be introduced in a follow-up PR. -/// Contains changed values during upgrades and downgrades of CRDs. -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] -pub struct ChangedValues { - /// List of values needed when downgrading to a particular version. - pub downgrades: HashMap>, - - /// List of values needed when upgrading to a particular version. - pub upgrades: HashMap>, -} - -/// Contains a changed value for a single field of the CRD. -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] -pub struct ChangedValue { - /// The name of the field of the custom resource this value is for. - pub name: String, - - /// The value to be used when upgrading or downgrading the custom resource. - #[schemars(schema_with = "raw_object_schema")] - pub value: serde_yaml::Value, -} - -// TODO (@Techassi): Think about where this should live. Basically this already exists in -// stackable-operator, but we cannot use it without depending on it which I would like to -// avoid. -fn raw_object_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema { - Schema::Object(SchemaObject { - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))), - extensions: [( - "x-kubernetes-preserve-unknown-fields".to_owned(), - serde_json::Value::Bool(true), - )] - .into(), - ..Default::default() - }) -} - -/// This error indicates that parsing an object from a conversion review failed. -#[derive(Debug, Snafu)] -pub enum ParseObjectError { - #[snafu(display("the field {field:?} is missing"))] - FieldMissing { field: String }, - - #[snafu(display("the field {field:?} must be a string"))] - FieldNotStr { field: String }, - - #[snafu(display("encountered unknown object API version {api_version:?}"))] - UnknownApiVersion { api_version: String }, - - #[snafu(display("failed to deserialize object from JSON"))] - Deserialize { source: serde_json::Error }, - - #[snafu(display("unexpected object kind {kind:?}, expected {expected:?}"))] - UnexpectedKind { kind: String, expected: String }, -} - -/// This error indicates that converting an object from a conversion review to the desired -/// version failed. -#[derive(Debug, Snafu)] -pub enum ConvertObjectError { - #[snafu(display("failed to parse object"))] - Parse { source: ParseObjectError }, - - #[snafu(display("failed to serialize object into json"))] - Serialize { source: serde_json::Error }, - - #[snafu(display("failed to parse desired API version"))] - ParseDesiredApiVersion { - source: UnknownDesiredApiVersionError, - }, -} - -impl ConvertObjectError { - /// Joins the error and its sources using colons. - pub fn join_errors(&self) -> String { - // NOTE (@Techassi): This can be done with itertools in a way shorter - // fashion but obviously brings in another dependency. Which of those - // two solutions performs better needs to evaluated. - // self.iter_chain().join(": ") - self.iter_chain() - .map(|err| err.to_string()) - .collect::>() - .join(": ") - } - - /// Returns a HTTP status code based on the underlying error. - pub fn http_status_code(&self) -> u16 { - match self { - ConvertObjectError::Parse { .. } => 400, - ConvertObjectError::Serialize { .. } => 500, - - // This is likely the clients fault, as it is requesting a unsupported version - ConvertObjectError::ParseDesiredApiVersion { - source: UnknownDesiredApiVersionError { .. }, - } => 400, - } - } -} - -#[derive(Debug, Snafu)] -#[snafu(display("unknown API version {api_version:?}"))] -pub struct UnknownDesiredApiVersionError { - pub api_version: String, -} diff --git a/crates/stackable-versioned/src/lib.rs b/crates/stackable-versioned/src/lib.rs index 5538eae65..5a768b19b 100644 --- a/crates/stackable-versioned/src/lib.rs +++ b/crates/stackable-versioned/src/lib.rs @@ -9,11 +9,147 @@ //! //! See [`versioned`] for an in-depth usage guide and a list of supported arguments. -// Re-exports +use std::collections::HashMap; + +use k8s_version::Version; +use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec}; +use snafu::{ErrorCompat, Snafu}; +// Re-export pub use stackable_versioned_macros::versioned; -// Behind k8s feature -#[cfg(feature = "k8s")] -mod k8s; -#[cfg(feature = "k8s")] -pub use k8s::*; +/// A value-to-value conversion that consumes the input value while tracking changes via a +/// Kubernetes status. +/// +/// This allows nested sub structs to bubble up their tracked changes. +pub trait TrackingFrom +where + Self: Sized, + S: TrackingStatus + Default, +{ + /// Describes the parent field of `Self`. None indicates that `Self` is at the root. + const PARENT: Option<&str>; + + /// Convert `T` into `Self`. + fn tracking_from(value: T, status: &mut S) -> Self; +} + +/// A value-to-value conversion that consumes the input value while tracking changes via a +/// Kubernetes status. The opposite of [`TrackingFrom`]. +/// +/// One should avoid implementing [`TrackingInto`] as it is automatically implemented via a +/// blanket implementation. +pub trait TrackingInto +where + Self: Sized, + S: TrackingStatus + Default, +{ + /// Convert `Self` into `T`. + fn tracking_into(self, status: &mut S) -> T; +} + +impl TrackingInto for T +where + S: TrackingStatus + Default, + U: TrackingFrom, +{ + fn tracking_into(self, status: &mut S) -> U { + U::tracking_from(self, status) + } +} + +/// Used to access [`ChangedValues`] from any status. +pub trait TrackingStatus { + fn changes(&mut self) -> &mut ChangedValues; +} + +// NOTE (@Techassi): This struct represents a rough first draft of how tracking values across +// CRD versions can be achieved. It is currently untested and unproven and might change down the +// line. Currently, this struct is only generated by the macro but not actually used by any other +// code. The tracking itself will be introduced in a follow-up PR. +/// Contains changed values during upgrades and downgrades of CRDs. +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +pub struct ChangedValues { + /// List of values needed when downgrading to a particular version. + pub downgrades: HashMap>, + + /// List of values needed when upgrading to a particular version. + pub upgrades: HashMap>, + // TODO (@Techassi): Add a version indicator here if we ever decide to change the tracking + // mechanism. +} + +/// Contains a changed value for a single field of the CRD. +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct ChangedValue { + /// The name of the field of the custom resource this value is for. + pub field_name: String, + + /// The value to be used when upgrading or downgrading the custom resource. + #[schemars(schema_with = "raw_object_schema")] + pub value: serde_yaml::Value, +} + +// TODO (@Techassi): Think about where this should live. Basically this already exists in +// stackable-operator, but we cannot use it without depending on it which I would like to +// avoid. +fn raw_object_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema { + Schema::Object(SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))), + extensions: [( + "x-kubernetes-preserve-unknown-fields".to_owned(), + serde_json::Value::Bool(true), + )] + .into(), + ..Default::default() + }) +} + +/// This error indicates that parsing an object from a conversion review failed. +#[derive(Debug, Snafu)] +pub enum ParseObjectError { + #[snafu(display(r#"failed to find "apiVersion" field"#))] + FieldNotPresent, + + #[snafu(display(r#"the "apiVersion" field must be a string"#))] + FieldNotStr, + + #[snafu(display("encountered unknown object API version {api_version:?}"))] + UnknownApiVersion { api_version: String }, + + #[snafu(display("failed to deserialize object from JSON"))] + Deserialize { source: serde_json::Error }, +} + +/// This error indicates that converting an object from a conversion review to the desired +/// version failed. +#[derive(Debug, Snafu)] +pub enum ConvertObjectError { + #[snafu(display("failed to parse object"))] + Parse { source: ParseObjectError }, + + #[snafu(display("failed to serialize object into json"))] + Serialize { source: serde_json::Error }, +} + +impl ConvertObjectError { + /// Joins the error and its sources using colons. + pub fn join_errors(&self) -> String { + // NOTE (@Techassi): This can be done with itertools in a way shorter + // fashion but obviously brings in another dependency. Which of those + // two solutions performs better needs to evaluated. + // self.iter_chain().join(": ") + self.iter_chain() + .map(|err| err.to_string()) + .collect::>() + .join(": ") + } + + /// Returns a HTTP status code based on the underlying error. + pub fn http_status_code(&self) -> u16 { + match self { + ConvertObjectError::Parse { .. } => 400, + ConvertObjectError::Serialize { .. } => 500, + } + } +} From 9583068d679353ebf03b7e25aed4ebef90053604 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 26 Jun 2025 16:36:27 +0200 Subject: [PATCH 05/16] chore: Adjust CRD code to reflect attribute changes --- .../src/crd/authentication/core/mod.rs | 16 ++++++++------ .../src/crd/authentication/kerberos/mod.rs | 16 ++++++++------ .../src/crd/authentication/tls/mod.rs | 22 ++++++++++--------- .../src/crd/listener/class/mod.rs | 2 +- .../src/crd/listener/listeners/mod.rs | 4 ++-- .../src/crd/s3/bucket/mod.rs | 16 ++++++++------ .../src/crd/s3/connection/mod.rs | 16 ++++++++------ crates/xtask/src/crd/dummy.rs | 22 ++++++++++--------- 8 files changed, 63 insertions(+), 51 deletions(-) diff --git a/crates/stackable-operator/src/crd/authentication/core/mod.rs b/crates/stackable-operator/src/crd/authentication/core/mod.rs index 9287072e3..f466faf5f 100644 --- a/crates/stackable-operator/src/crd/authentication/core/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/core/mod.rs @@ -6,7 +6,14 @@ use crate::versioned::versioned; mod v1alpha1_impl; -#[versioned(version(name = "v1alpha1"))] +#[versioned( + version(name = "v1alpha1"), + crates( + kube_core = "kube::core", + k8s_openapi = "k8s_openapi", + schemars = "schemars", + ) +)] pub mod versioned { pub mod v1alpha1 { // Re-export the v1alpha1-specific error type from the private impl module. @@ -26,14 +33,9 @@ pub mod versioned { /// /// [1]: DOCS_BASE_URL_PLACEHOLDER/concepts/authentication /// [2]: DOCS_BASE_URL_PLACEHOLDER/tutorials/authentication_with_openldap - #[versioned(k8s( + #[versioned(crd( group = "authentication.stackable.tech", plural = "authenticationclasses", - crates( - kube_core = "kube::core", - k8s_openapi = "k8s_openapi", - schemars = "schemars", - ) ))] #[derive( Clone, diff --git a/crates/stackable-operator/src/crd/authentication/kerberos/mod.rs b/crates/stackable-operator/src/crd/authentication/kerberos/mod.rs index afff0b374..ccb2d4396 100644 --- a/crates/stackable-operator/src/crd/authentication/kerberos/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/kerberos/mod.rs @@ -3,11 +3,13 @@ use serde::{Deserialize, Serialize}; use stackable_versioned::versioned; #[versioned(version(name = "v1alpha1"))] -#[derive( - Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, -)] -#[serde(rename_all = "camelCase")] -pub struct AuthenticationProvider { - /// Mandatory SecretClass used to obtain keytabs. - pub kerberos_secret_class: String, +pub mod versioned { + #[derive( + Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, + )] + #[serde(rename_all = "camelCase")] + pub struct AuthenticationProvider { + /// Mandatory SecretClass used to obtain keytabs. + pub kerberos_secret_class: String, + } } diff --git a/crates/stackable-operator/src/crd/authentication/tls/mod.rs b/crates/stackable-operator/src/crd/authentication/tls/mod.rs index 38bdcb633..cf44a145f 100644 --- a/crates/stackable-operator/src/crd/authentication/tls/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/tls/mod.rs @@ -3,14 +3,16 @@ use serde::{Deserialize, Serialize}; use stackable_versioned::versioned; #[versioned(version(name = "v1alpha1"))] -#[derive( - Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, -)] -#[serde(rename_all = "camelCase")] -pub struct AuthenticationProvider { - /// See [ADR017: TLS authentication](DOCS_BASE_URL_PLACEHOLDER/contributor/adr/adr017-tls_authentication). - /// If `client_cert_secret_class` is not set, the TLS settings may also be used for client authentication. - /// If `client_cert_secret_class` is set, the [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) - /// will be used to provision client certificates. - pub client_cert_secret_class: Option, +pub mod versioned { + #[derive( + Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, + )] + #[serde(rename_all = "camelCase")] + pub struct AuthenticationProvider { + /// See [ADR017: TLS authentication](DOCS_BASE_URL_PLACEHOLDER/contributor/adr/adr017-tls_authentication). + /// If `client_cert_secret_class` is not set, the TLS settings may also be used for client authentication. + /// If `client_cert_secret_class` is set, the [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) + /// will be used to provision client certificates. + pub client_cert_secret_class: Option, + } } diff --git a/crates/stackable-operator/src/crd/listener/class/mod.rs b/crates/stackable-operator/src/crd/listener/class/mod.rs index ecbd50109..25054e26f 100644 --- a/crates/stackable-operator/src/crd/listener/class/mod.rs +++ b/crates/stackable-operator/src/crd/listener/class/mod.rs @@ -25,7 +25,7 @@ pub mod versioned { /// Defines a policy for how [Listeners](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener) should be exposed. /// Read the [ListenerClass documentation](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass) /// for more information. - #[versioned(k8s(group = "listeners.stackable.tech"))] + #[versioned(crd(group = "listeners.stackable.tech"))] #[derive(CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ListenerClassSpec { diff --git a/crates/stackable-operator/src/crd/listener/listeners/mod.rs b/crates/stackable-operator/src/crd/listener/listeners/mod.rs index 095eeb9d0..1d2fdf93a 100644 --- a/crates/stackable-operator/src/crd/listener/listeners/mod.rs +++ b/crates/stackable-operator/src/crd/listener/listeners/mod.rs @@ -49,7 +49,7 @@ pub mod versioned { /// ["sticky" scheduling](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener#_sticky_scheduling). /// /// Learn more in the [Listener documentation](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener). - #[versioned(k8s( + #[versioned(crd( group = "listeners.stackable.tech", status = "v1alpha1::ListenerStatus", namespaced @@ -79,7 +79,7 @@ pub mod versioned { /// This is not expected to be created or modified by users. It will be created by /// the Stackable Listener Operator when mounting the listener volume, and is always /// named `pod-{pod.metadata.uid}`. - #[versioned(k8s( + #[versioned(crd( group = "listeners.stackable.tech", plural = "podlisteners", namespaced, diff --git a/crates/stackable-operator/src/crd/s3/bucket/mod.rs b/crates/stackable-operator/src/crd/s3/bucket/mod.rs index 335835c84..9ec56d2a8 100644 --- a/crates/stackable-operator/src/crd/s3/bucket/mod.rs +++ b/crates/stackable-operator/src/crd/s3/bucket/mod.rs @@ -6,7 +6,14 @@ use crate::{crd::s3::connection::v1alpha1 as conn_v1alpha1, versioned::versioned mod v1alpha1_impl; -#[versioned(version(name = "v1alpha1"))] +#[versioned( + version(name = "v1alpha1"), + crates( + kube_core = "kube::core", + k8s_openapi = "k8s_openapi", + schemars = "schemars", + ) +)] pub mod versioned { pub mod v1alpha1 { pub use v1alpha1_impl::BucketError; @@ -14,15 +21,10 @@ pub mod versioned { /// S3 bucket specification containing the bucket name and an inlined or referenced connection specification. /// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). - #[versioned(k8s( + #[versioned(crd( group = "s3.stackable.tech", kind = "S3Bucket", plural = "s3buckets", - crates( - kube_core = "kube::core", - k8s_openapi = "k8s_openapi", - schemars = "schemars", - ), namespaced ))] #[derive(Clone, CustomResource, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] diff --git a/crates/stackable-operator/src/crd/s3/connection/mod.rs b/crates/stackable-operator/src/crd/s3/connection/mod.rs index 97c754e2c..b41c280d8 100644 --- a/crates/stackable-operator/src/crd/s3/connection/mod.rs +++ b/crates/stackable-operator/src/crd/s3/connection/mod.rs @@ -16,7 +16,14 @@ mod v1alpha1_impl; /// Use this type in you operator! pub type ResolvedConnection = v1alpha1::ConnectionSpec; -#[versioned(version(name = "v1alpha1"))] +#[versioned( + version(name = "v1alpha1"), + crates( + kube_core = "kube::core", + k8s_openapi = "k8s_openapi", + schemars = "schemars", + ) +)] pub mod versioned { pub mod v1alpha1 { pub use v1alpha1_impl::ConnectionError; @@ -24,15 +31,10 @@ pub mod versioned { /// S3 connection definition as a resource. /// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). - #[versioned(k8s( + #[versioned(crd( group = "s3.stackable.tech", kind = "S3Connection", plural = "s3connections", - crates( - kube_core = "kube::core", - k8s_openapi = "k8s_openapi", - schemars = "schemars", - ), namespaced ))] #[derive(CustomResource, Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] diff --git a/crates/xtask/src/crd/dummy.rs b/crates/xtask/src/crd/dummy.rs index 6c5fd7871..3f0652e80 100644 --- a/crates/xtask/src/crd/dummy.rs +++ b/crates/xtask/src/crd/dummy.rs @@ -4,25 +4,27 @@ use stackable_operator::{ config::fragment::Fragment, kube::CustomResource, role_utils::Role, - schemars::JsonSchema, + schemars::{self, JsonSchema}, status::condition::ClusterCondition, versioned::versioned, }; -#[versioned(version(name = "v1alpha1"))] +#[versioned( + version(name = "v1alpha1"), + crates( + kube_core = "stackable_operator::kube::core", + kube_client = "stackable_operator::kube::client", + k8s_openapi = "stackable_operator::k8s_openapi", + schemars = "stackable_operator::schemars", + versioned = "stackable_operator::versioned" + ) +)] pub mod versioned { - #[versioned(k8s( + #[versioned(crd( group = "dummy.stackable.tech", kind = "DummyCluster", status = "v1alpha1::DummyClusterStatus", namespaced, - crates( - kube_core = "stackable_operator::kube::core", - kube_client = "stackable_operator::kube::client", - k8s_openapi = "stackable_operator::k8s_openapi", - schemars = "stackable_operator::schemars", - versioned = "stackable_operator::versioned" - ) ))] #[derive(Clone, CustomResource, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] #[schemars(crate = "stackable_operator::schemars")] From 400927124002077679f09ad2c5658d279125ddf5 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 26 Jun 2025 16:42:11 +0200 Subject: [PATCH 06/16] feat!(stackable-versioned): Add conversion tracking --- .../src/attrs/container.rs | 2 +- .../src/attrs/item/field.rs | 7 +- .../src/attrs/item/mod.rs | 2 +- .../src/codegen/changes.rs | 2 +- .../src/codegen/container/enum.rs | 63 +- .../src/codegen/container/mod.rs | 174 ++-- .../codegen/container/struct/conversion.rs | 819 ++++++++++++++++++ .../src/codegen/container/struct/merge.rs | 40 + .../src/codegen/container/struct/mod.rs | 481 ++++++++-- .../src/codegen/item/field.rs | 196 ++++- .../src/codegen/item/mod.rs | 45 + .../src/codegen/item/variant.rs | 4 +- .../src/codegen/mod.rs | 72 +- .../src/codegen/module.rs | 165 ++-- crates/stackable-versioned-macros/src/lib.rs | 39 +- crates/stackable-versioned/src/lib.rs | 50 +- 16 files changed, 1777 insertions(+), 384 deletions(-) create mode 100644 crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs create mode 100644 crates/stackable-versioned-macros/src/codegen/container/struct/merge.rs diff --git a/crates/stackable-versioned-macros/src/attrs/container.rs b/crates/stackable-versioned-macros/src/attrs/container.rs index d75045ec6..3a44ac4c1 100644 --- a/crates/stackable-versioned-macros/src/attrs/container.rs +++ b/crates/stackable-versioned-macros/src/attrs/container.rs @@ -13,7 +13,7 @@ pub struct ContainerAttributes { impl ContainerAttributes { fn validate(self) -> Result { - if self.crd_arguments.is_some() + if self.crd_arguments.is_none() && (self.skip.object_from.is_present() || self.skip.merged_crd.is_present() || self.skip.try_convert.is_present()) diff --git a/crates/stackable-versioned-macros/src/attrs/item/field.rs b/crates/stackable-versioned-macros/src/attrs/item/field.rs index b8b5ad0dc..f71562503 100644 --- a/crates/stackable-versioned-macros/src/attrs/item/field.rs +++ b/crates/stackable-versioned-macros/src/attrs/item/field.rs @@ -1,4 +1,4 @@ -use darling::{FromField, Result}; +use darling::{FromField, Result, util::Flag}; use syn::{Attribute, Ident}; use crate::{attrs::item::CommonItemAttributes, codegen::VersionDefinition, utils::FieldIdent}; @@ -36,6 +36,11 @@ pub struct FieldAttributes { // FromMeta. /// The original attributes for the field. pub attrs: Vec, + + /// Indicates that this field's type is a nested sub struct. The indicator + /// is needed to let the macro know to generate conversion code with support + /// for tracking across struct boundaries. + pub nested: Flag, } impl FieldAttributes { diff --git a/crates/stackable-versioned-macros/src/attrs/item/mod.rs b/crates/stackable-versioned-macros/src/attrs/item/mod.rs index 415a514cb..6cb91476c 100644 --- a/crates/stackable-versioned-macros/src/attrs/item/mod.rs +++ b/crates/stackable-versioned-macros/src/attrs/item/mod.rs @@ -7,7 +7,7 @@ use quote::format_ident; use syn::{Attribute, Path, Type, spanned::Spanned}; use crate::{ - codegen::{ItemStatus, VersionDefinition}, + codegen::{VersionDefinition, item::ItemStatus}, utils::ItemIdentExt, }; diff --git a/crates/stackable-versioned-macros/src/codegen/changes.rs b/crates/stackable-versioned-macros/src/codegen/changes.rs index 31e9a6dab..c2baa39be 100644 --- a/crates/stackable-versioned-macros/src/codegen/changes.rs +++ b/crates/stackable-versioned-macros/src/codegen/changes.rs @@ -3,7 +3,7 @@ use std::{collections::BTreeMap, ops::Bound}; use k8s_version::Version; use syn::Type; -use crate::codegen::{ItemStatus, VersionDefinition}; +use crate::codegen::{VersionDefinition, item::ItemStatus}; pub trait Neighbors where diff --git a/crates/stackable-versioned-macros/src/codegen/container/enum.rs b/crates/stackable-versioned-macros/src/codegen/container/enum.rs index 4abbe1c4f..8a85e4cef 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/enum.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/enum.rs @@ -6,12 +6,15 @@ use quote::quote; use syn::{Generics, ItemEnum}; use crate::{ - attrs::container::NestedContainerAttributes, + attrs::container::ContainerAttributes, codegen::{ - ItemStatus, StandaloneContainerAttributes, VersionDefinition, + Direction, VersionContext, VersionDefinition, changes::Neighbors, - container::{CommonContainerData, Container, ContainerIdents, ContainerOptions, Direction}, - item::VersionedVariant, + container::{ + CommonContainerData, Container, ContainerIdents, ContainerOptions, ContainerTokens, + ExtendContainerTokens, ModuleGenerationContext, + }, + item::{ItemStatus, VersionedVariant}, }, }; @@ -27,11 +30,13 @@ impl Container { } let options = ContainerOptions { - kubernetes_arguments: None, - skip_from: attributes.options.skip.is_some_and(|s| s.from.is_present()), + skip_from: attributes.skip.from.is_present(), + skip_object_from: attributes.skip.object_from.is_present(), + skip_merged_crd: attributes.skip.merged_crd.is_present(), + skip_try_convert: attributes.skip.try_convert.is_present(), }; - let idents = ContainerIdents::from(item_enum.ident, None); + let idents = ContainerIdents::from(item_enum.ident); let common = CommonContainerData { original_attributes: item_enum.attrs, @@ -62,18 +67,43 @@ pub struct Enum { // Common token generation impl Enum { + pub fn generate_tokens<'a>( + &'a self, + versions: &'a [VersionDefinition], + gen_ctx: ModuleGenerationContext<'a>, + ) -> ContainerTokens<'a> { + let mut versions = versions.iter().peekable(); + let mut container_tokens = ContainerTokens::default(); + + while let Some(version) = versions.next() { + let next_version = versions.peek().copied(); + let ver_ctx = VersionContext::new(version, next_version); + + let enum_definition = self.generate_definition(ver_ctx); + let upgrade_from = self.generate_from_impl(Direction::Upgrade, ver_ctx, gen_ctx); + let downgrade_from = self.generate_from_impl(Direction::Downgrade, ver_ctx, gen_ctx); + + container_tokens + .extend_inner(&version.inner, enum_definition) + .extend_between(&version.inner, upgrade_from) + .extend_between(&version.inner, downgrade_from); + } + + container_tokens + } + /// Generates code for the enum definition. - pub fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { + pub fn generate_definition(&self, ver_ctx: VersionContext<'_>) -> TokenStream { let where_clause = self.generics.where_clause.as_ref(); let type_generics = &self.generics; let original_attributes = &self.common.original_attributes; let ident = &self.common.idents.original; - let version_docs = &version.docs; + let version_docs = &ver_ctx.version.docs; let mut variants = TokenStream::new(); for variant in &self.variants { - variants.extend(variant.generate_for_container(version)); + variants.extend(variant.generate_for_container(ver_ctx.version)); } quote! { @@ -89,15 +119,16 @@ impl Enum { pub fn generate_from_impl( &self, direction: Direction, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - add_attributes: bool, + ver_ctx: VersionContext<'_>, + gen_ctx: ModuleGenerationContext<'_>, ) -> Option { - if version.skip_from || self.common.options.skip_from { + if ver_ctx.version.skip_from || self.common.options.skip_from { return None; } - next_version.map(|next_version| { + let version = ver_ctx.version; + + ver_ctx.next_version.map(|next_version| { // TODO (@Techassi): Support generic types which have been removed in newer versions, // but need to exist for older versions How do we represent that? Because the // defined struct always represents the latest version. I guess we could generally @@ -118,7 +149,7 @@ impl Enum { // Only add the #[automatically_derived] attribute only if this impl is used // outside of a module (in standalone mode). - let automatically_derived = add_attributes + let automatically_derived = gen_ctx.add_attributes .not() .then(|| quote! {#[automatically_derived]}); diff --git a/crates/stackable-versioned-macros/src/codegen/container/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/mod.rs index 94db6cbd8..43b1cbd5e 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/mod.rs @@ -1,13 +1,17 @@ -use darling::{Result, util::IdentString}; -use proc_macro2::{Span, TokenStream}; -use quote::{format_ident, quote}; -use syn::{Attribute, Ident, ItemEnum, ItemStruct, Visibility}; +use std::collections::HashMap; + +use darling::util::IdentString; +use k8s_version::Version; +use proc_macro2::{Span, TokenStream, TokenTree}; +use quote::format_ident; +use syn::{Attribute, Ident}; use crate::{ - attrs::container::{StandaloneContainerAttributes, k8s::KubernetesArguments}, + attrs::container::StructCrdArguments, codegen::{ - KubernetesTokens, VersionDefinition, + VersionDefinition, container::{r#enum::Enum, r#struct::Struct}, + module::ModuleGenerationContext, }, utils::ContainerIdentExt, }; @@ -37,57 +41,117 @@ pub enum Container { Enum(Enum), } -impl Container { - /// Generates the container definition for the specified `version`. - pub fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { - match self { - Container::Struct(s) => s.generate_definition(version), - Container::Enum(e) => e.generate_definition(version), - } +#[derive(Debug, Default)] +pub struct ContainerTokens<'a> { + pub versioned: HashMap<&'a Version, VersionedContainerTokens>, + pub outer: TokenStream, +} + +#[derive(Debug, Default)] +/// A collection of generated tokens for a container per version. +pub struct VersionedContainerTokens { + /// The inner tokens are placed inside the version module. These tokens mostly only include the + /// container definition with attributes, doc comments, etc. + pub inner: TokenStream, + + /// These tokens are placed between version modules. These could technically be grouped together + /// with the outer tokens, but it makes sense to keep them separate to achieve a more structured + /// code generation. These tokens mostly only include `From` impls to convert between two versions + pub between: TokenStream, +} + +pub trait ExtendContainerTokens<'a, T> { + fn extend_inner>( + &mut self, + version: &'a Version, + streams: I, + ) -> &mut Self; + fn extend_between>( + &mut self, + version: &'a Version, + streams: I, + ) -> &mut Self; + fn extend_outer>(&mut self, streams: I) -> &mut Self; +} + +impl<'a> ExtendContainerTokens<'a, TokenStream> for ContainerTokens<'a> { + fn extend_inner>( + &mut self, + version: &'a Version, + streams: I, + ) -> &mut Self { + self.versioned + .entry(version) + .or_default() + .inner + .extend(streams); + self } - pub fn generate_from_impl( - &self, - direction: Direction, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - add_attributes: bool, - ) -> Option { - match self { - Container::Struct(s) => { - // TODO (@Techassi): Decide here (based on K8s args) what we want to generate - s.generate_from_impl(direction, version, next_version, add_attributes) - } - Container::Enum(e) => { - e.generate_from_impl(direction, version, next_version, add_attributes) - } - } + fn extend_between>( + &mut self, + version: &'a Version, + streams: I, + ) -> &mut Self { + self.versioned + .entry(version) + .or_default() + .between + .extend(streams); + self } - /// Generates Kubernetes specific code for the container. - /// - /// This includes CRD merging, CRD conversion, and the conversion tracking status struct. - pub fn generate_kubernetes_code( - &self, - versions: &[VersionDefinition], - tokens: &KubernetesTokens, - vis: &Visibility, - is_nested: bool, - ) -> Option { - match self { - Container::Struct(s) => s.generate_kubernetes_code(versions, tokens, vis, is_nested), - Container::Enum(_) => None, - } + fn extend_outer>(&mut self, streams: I) -> &mut Self { + self.outer.extend(streams); + self + } +} + +impl<'a> ExtendContainerTokens<'a, TokenTree> for ContainerTokens<'a> { + fn extend_inner>( + &mut self, + version: &'a Version, + streams: I, + ) -> &mut Self { + self.versioned + .entry(version) + .or_default() + .inner + .extend(streams); + self + } + + fn extend_between>( + &mut self, + version: &'a Version, + streams: I, + ) -> &mut Self { + self.versioned + .entry(version) + .or_default() + .between + .extend(streams); + self } - /// Generates KUbernetes specific code for individual versions. - pub fn generate_kubernetes_version_items( - &self, - version: &VersionDefinition, - ) -> Option<(TokenStream, IdentString, TokenStream, String)> { + fn extend_outer>(&mut self, streams: I) -> &mut Self { + self.outer.extend(streams); + self + } +} + +impl Container { + // TODO (@Techassi): Only have a single function here. It should return and store all generated + // tokens. It should also have access to a single GenerationContext, which provides all external + // parameters which influence code generation. + pub fn generate_tokens<'a>( + &'a self, + versions: &'a [VersionDefinition], + ctx: ModuleGenerationContext<'a>, + ) -> ContainerTokens<'a> { match self { - Container::Struct(s) => s.generate_kubernetes_version_items(version), - Container::Enum(_) => None, + Container::Struct(s) => s.generate_tokens(versions, ctx), + Container::Enum(e) => e.generate_tokens(versions, ctx), } } @@ -162,12 +226,8 @@ impl KubernetesIdents { #[derive(Debug)] pub struct ContainerOptions { - pub kubernetes_arguments: Option, pub skip_from: bool, -} - -#[derive(Copy, Clone, Debug)] -pub enum Direction { - Upgrade, - Downgrade, + pub skip_object_from: bool, + pub skip_merged_crd: bool, + pub skip_try_convert: bool, } diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs new file mode 100644 index 000000000..9229492a9 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs @@ -0,0 +1,819 @@ +use std::{borrow::Cow, cmp::Ordering, ops::Not as _}; + +use indoc::formatdoc; +use itertools::Itertools as _; +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse_quote; + +use crate::{ + codegen::{ + Direction, VersionContext, VersionDefinition, + changes::Neighbors as _, + container::r#struct::{SpecGenerationContext, Struct}, + item::ItemStatus, + module::ModuleGenerationContext, + }, + utils::{doc_comments::DocComments as _, path_to_string}, +}; + +const CONVERTED_OBJECT_COUNT_ATTRIBUTE: &str = "k8s.crd.conversion.converted_object_count"; +const DESIRED_API_VERSION_ATTRIBUTE: &str = "k8s.crd.conversion.desired_api_version"; +const API_VERSION_ATTRIBUTE: &str = "k8s.crd.conversion.api_version"; +const STEPS_ATTRIBUTE: &str = "k8s.crd.conversion.steps"; +const KIND_ATTRIBUTE: &str = "k8s.crd.conversion.kind"; + +#[derive(Debug, Default)] +pub struct TracingTokens { + pub successful_conversion_response_event: Option, + pub convert_objects_instrumentation: Option, + pub invalid_conversion_review_event: Option, + pub try_convert_instrumentation: Option, +} + +impl Struct { + /// Generates the Kubernetes specific From impl for the top-level object. + pub(super) fn generate_object_from_impl( + &self, + direction: Direction, + ver_ctx: VersionContext<'_>, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> Option { + if mod_gen_ctx.skip.object_from.is_present() || self.common.options.skip_object_from { + return None; + } + + if !mod_gen_ctx + .kubernetes_options + .experimental_conversion_tracking + .is_present() + { + return None; + } + + let next_version = ver_ctx.next_version; + let version = ver_ctx.version; + + next_version.map(|next_version| { + let from_struct_parameter_ident = &spec_gen_ctx.kubernetes_idents.parameter; + let object_struct_ident = &spec_gen_ctx.kubernetes_idents.kind; + let spec_struct_ident = &self.common.idents.original; + + let versioned_path = &*mod_gen_ctx.crates.versioned; + + let automatically_derived = mod_gen_ctx.automatically_derived_attr(); + + let (for_module_ident, from_module_ident) = match direction { + Direction::Upgrade => (&next_version.idents.module, &version.idents.module), + Direction::Downgrade => (&version.idents.module, &next_version.idents.module), + }; + + quote! { + #automatically_derived + impl ::std::convert::From<#from_module_ident::#object_struct_ident> for #for_module_ident::#object_struct_ident { + fn from(#from_struct_parameter_ident: #from_module_ident::#object_struct_ident) -> Self { + // The status is optional. The be able to track changes in nested sub structs it needs + // to be initialized with a default value. + let mut status = #from_struct_parameter_ident.status.unwrap_or_default(); + + // Convert the spec and track values in the status + let spec = + <#for_module_ident::#spec_struct_ident as #versioned_path::TrackingFrom<_, _>>::tracking_from( + #from_struct_parameter_ident.spec, + &mut status, + "", + ); + + // Construct the final object by copying over the metadata, setting the status and + // using the converted spec. + Self { + metadata: #from_struct_parameter_ident.metadata, + status: Some(status), + spec, + } + } + } + } + }) + } + + pub(super) fn generate_tracking_from_impl( + &self, + direction: Direction, + version: &VersionDefinition, + next_version: &VersionDefinition, + mod_gen_ctx: ModuleGenerationContext<'_>, + ) -> TokenStream { + // TODO (@Techassi): Support generic types which have been removed in newer versions, + // but need to exist for older versions How do we represent that? Because the + // defined struct always represents the latest version. I guess we could generally + // advise against using generic types, but if you have to, avoid removing it in + // later versions. + let from_struct_ident = &self.common.idents.parameter; + let struct_ident = &self.common.idents.original; + + let versioned_path = &*mod_gen_ctx.crates.versioned; + + // Include allow(deprecated) only when this or the next version is + // deprecated. Also include it, when a field in this or the next + // version is deprecated. + let allow_attribute = (version.deprecated.is_some() + || next_version.deprecated.is_some() + || self.is_any_field_deprecated(version) + || self.is_any_field_deprecated(next_version)) + .then(|| quote! { #[allow(deprecated)] }); + + // Only add the #[automatically_derived] attribute only if this impl is used + // outside of a module (in standalone mode). + let automatically_derived = mod_gen_ctx + .add_attributes + .not() + .then(|| quote! {#[automatically_derived]}); + + let fields = |direction: Direction| -> TokenStream { + self.fields + .iter() + .filter_map(|f| { + f.generate_for_from_impl(direction, version, next_version, from_struct_ident) + }) + .collect() + }; + + let (fields, for_module_ident, from_module_ident) = match direction { + direction @ Direction::Upgrade => { + let from_module_ident = &version.idents.module; + let for_module_ident = &next_version.idents.module; + + (fields(direction), for_module_ident, from_module_ident) + } + direction @ Direction::Downgrade => { + let from_module_ident = &next_version.idents.module; + let for_module_ident = &version.idents.module; + + (fields(direction), for_module_ident, from_module_ident) + } + }; + + let inserts = self.generate_tracking_inserts(direction, next_version, mod_gen_ctx); + let removals = self.generate_tracking_removals(direction, next_version, mod_gen_ctx); + let json_paths = self.generate_json_paths(next_version); + + // TODO (@Techassi): Re-add support for generics + // TODO (@Techassi): We know the status, so we can hard-code it, but hard to track across structs + + quote! { + #automatically_derived + #allow_attribute + impl #versioned_path::TrackingFrom<#from_module_ident::#struct_ident, S> for #for_module_ident::#struct_ident + where + S: #versioned_path::TrackingStatus + ::core::default::Default + { + #[allow(unused)] + fn tracking_from(#from_struct_ident: #from_module_ident::#struct_ident, status: &mut S, parent: &str) -> Self { + use #versioned_path::TrackingInto as _; + + #json_paths + // Depending on the direction, we need to either insert changed values into + // the upgrade or downgrade section. Only then we can convert the spec. + #inserts + + let mut spec = Self { + #fields + }; + + // After the spec is converted, depending on the direction, we need to apply + // changed values from either the upgrade or downgrade section. Afterwards + // we can return the successfully converted spec and the status contains + // the tracked changes. + #removals + spec + } + } + } + } + + fn generate_tracking_inserts( + &self, + direction: Direction, + next_version: &VersionDefinition, + mod_gen_ctx: ModuleGenerationContext<'_>, + ) -> Option { + if !self.needs_tracking(next_version) { + return None; + } + + match direction { + // This is only needed once we support removal of fields + Direction::Upgrade => None, + Direction::Downgrade => { + let next_version_string = next_version.inner.to_string(); + let from_struct_ident = &self.common.idents.parameter; + + let inserts: TokenStream = self + .fields + .iter() + .filter_map(|f| { + f.generate_for_status_insertion( + direction, + next_version, + from_struct_ident, + mod_gen_ctx, + ) + }) + .collect(); + + Some(quote! { + let upgrades = status + .changes() + .upgrades + .entry(#next_version_string.to_owned()) + .or_default(); + + #inserts + }) + } + } + } + + fn generate_tracking_removals( + &self, + direction: Direction, + next_version: &VersionDefinition, + mod_gen_ctx: ModuleGenerationContext<'_>, + ) -> Option { + if !self.needs_tracking(next_version) { + return None; + } + + let match_arms: TokenStream = self + .fields + .iter() + .filter_map(|f| f.generate_for_status_removal(direction, next_version)) + .collect(); + + match direction { + Direction::Upgrade => { + let next_version_string = next_version.inner.to_string(); + let versioned_path = &*mod_gen_ctx.crates.versioned; + + Some(quote! { + // NOTE (@Techassi): This is an awkward thing to do. Can we possibly use &str for the keys here? + if let Some(upgrades) = status.changes().upgrades.remove(&#next_version_string.to_owned()) { + for #versioned_path::ChangedValue { field_name, value } in upgrades { + match field_name { + #match_arms + _ => unreachable!(), + } + } + } + }) + } + // This is only needed once we support removal of fields + Direction::Downgrade => None, + } + } + + fn generate_json_paths(&self, next_version: &VersionDefinition) -> Option { + // if !self.needs_tracking(next_version) { + // return None; + // } + + let json_paths = self + .fields + .iter() + .filter_map(|f| f.generate_for_json_path(next_version)) + .collect(); + + Some(json_paths) + } + + pub(super) fn needs_tracking(&self, version: &VersionDefinition) -> bool { + self.fields.iter().any(|f| { + f.changes.as_ref().is_some_and(|c| { + c.value_is(&version.inner, |s| { + // For now, only added fields need to be tracked. In the future, removals and + // type changes also need to be tracked + matches!(s, ItemStatus::Addition { .. }) + }) + }) + }) + } + + pub(super) fn generate_try_convert_fn( + &self, + versions: &[VersionDefinition], + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> Option { + if mod_gen_ctx.skip.try_convert.is_present() || self.common.options.skip_try_convert { + return None; + } + + let version_enum_ident = &spec_gen_ctx.kubernetes_idents.version; + let struct_ident = &spec_gen_ctx.kubernetes_idents.kind; + + let kube_client_path = &*mod_gen_ctx.crates.kube_client; + let serde_json_path = &*mod_gen_ctx.crates.serde_json; + let kube_core_path = &*mod_gen_ctx.crates.kube_core; + let versioned_path = &*mod_gen_ctx.crates.versioned; + + let convert_object_error = quote! { #versioned_path::ConvertObjectError }; + + // Generate conversion paths and the match arms for these paths + let match_arms = self.generate_conversion_match_arms(versions, mod_gen_ctx, spec_gen_ctx); + + // TODO (@Techassi): Make this a feature, drop the option from the macro arguments + // Generate tracing attributes and events if tracing is enabled + let TracingTokens { + successful_conversion_response_event, + convert_objects_instrumentation, + invalid_conversion_review_event, + try_convert_instrumentation, + } = self.generate_conversion_tracing(mod_gen_ctx, spec_gen_ctx); + + // Generate doc comments + let conversion_review_reference = + path_to_string(&parse_quote! { #kube_core_path::conversion::ConversionReview }); + + let docs = formatdoc! {" + Tries to convert a list of objects of kind [`{struct_ident}`] to the desired API version + specified in the [`ConversionReview`][cr]. + + The returned [`ConversionReview`][cr] either indicates a success or a failure, which + is handed back to the Kubernetes API server. + + [cr]: {conversion_review_reference}" + } + .into_doc_comments(); + + Some(quote! { + #(#[doc = #docs])* + #try_convert_instrumentation + pub fn try_convert(review: #kube_core_path::conversion::ConversionReview) + -> #kube_core_path::conversion::ConversionReview + { + // First, turn the review into a conversion request + let request = match #kube_core_path::conversion::ConversionRequest::from_review(review) { + ::std::result::Result::Ok(request) => request, + ::std::result::Result::Err(err) => { + #invalid_conversion_review_event + + return #kube_core_path::conversion::ConversionResponse::invalid( + #kube_client_path::Status { + status: Some(#kube_core_path::response::StatusSummary::Failure), + message: err.to_string(), + reason: err.to_string(), + details: None, + code: 400, + } + ).into_review() + } + }; + + // Convert all objects into the desired version + let response = match Self::convert_objects(request.objects, &request.desired_api_version) { + ::std::result::Result::Ok(converted_objects) => { + #successful_conversion_response_event + + // We construct the response from the ground up as the helper functions + // don't provide any benefit over manually doing it. Constructing a + // ConversionResponse via for_request is not possible due to a partial move + // of request.objects. The function internally doesn't even use the list of + // objects. The success function on ConversionResponse basically only sets + // the result to success and the converted objects to the provided list. + // The below code does the same thing. + #kube_core_path::conversion::ConversionResponse { + result: #kube_client_path::Status::success(), + types: request.types, + uid: request.uid, + converted_objects, + } + }, + ::std::result::Result::Err(err) => { + let code = err.http_status_code(); + let message = err.join_errors(); + + #kube_core_path::conversion::ConversionResponse { + result: #kube_client_path::Status { + status: Some(#kube_core_path::response::StatusSummary::Failure), + message: message.clone(), + reason: message, + details: None, + code, + }, + types: request.types, + uid: request.uid, + converted_objects: vec![], + } + }, + }; + + response.into_review() + } + + #convert_objects_instrumentation + fn convert_objects( + objects: ::std::vec::Vec<#serde_json_path::Value>, + desired_api_version: &str, + ) + -> ::std::result::Result<::std::vec::Vec<#serde_json_path::Value>, #convert_object_error> + { + let desired_api_version = #version_enum_ident::from_api_version(desired_api_version) + .map_err(|source| #convert_object_error::ParseDesiredApiVersion { source })?; + + let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); + + for object in objects { + // This clone is required because in the noop case we move the object into + // the converted objects vec. + let current_object = Self::from_json_value(object.clone()) + .map_err(|source| #convert_object_error::Parse { source })?; + + match (current_object, desired_api_version) { + #(#match_arms,)* + // If no match arm matches, this is a noop. This is the case if the desired + // version matches the current object api version. + // NOTE (@Techassi): I'm curious if this will ever happen? In theory the K8s + // apiserver should never send such a conversion review. + _ => converted_objects.push(object), + } + } + + ::std::result::Result::Ok(converted_objects) + } + }) + } + + pub(super) fn generate_status_struct( + &self, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> Option { + if mod_gen_ctx.skip.try_convert.is_present() || self.common.options.skip_try_convert { + return None; + } + + if !mod_gen_ctx + .kubernetes_options + .experimental_conversion_tracking + .is_present() + { + return None; + } + + let status_ident = &spec_gen_ctx.kubernetes_idents.status; + + let versioned_path = &*mod_gen_ctx.crates.versioned; + let schemars_path = &*mod_gen_ctx.crates.schemars; + let serde_path = &*mod_gen_ctx.crates.serde; + + let automatically_derived = mod_gen_ctx.automatically_derived_attr(); + + // TODO (@Techassi): Validate that users don't specify the status we generate + let status = spec_gen_ctx + .kubernetes_arguments + .status + .as_ref() + .map(|status| { + quote! { + #[serde(flatten)] + pub status: #status, + } + }); + + Some(quote! { + #automatically_derived + #[derive( + ::core::clone::Clone, + ::core::default::Default, + ::core::fmt::Debug, + #serde_path::Deserialize, + #serde_path::Serialize, + #schemars_path::JsonSchema + )] + #[serde(rename_all = "camelCase")] + pub struct #status_ident { + pub changed_values: #versioned_path::ChangedValues, + + #status + } + + #automatically_derived + impl #versioned_path::TrackingStatus for #status_ident { + fn changes(&mut self) -> &mut #versioned_path::ChangedValues { + &mut self.changed_values + } + } + }) + } + + pub(super) fn generate_from_json_value_fn( + &self, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> Option { + if mod_gen_ctx.skip.try_convert.is_present() || self.common.options.skip_try_convert { + return None; + } + + let serde_json_path = &*mod_gen_ctx.crates.serde_json; + let versioned_path = &*mod_gen_ctx.crates.versioned; + + let parse_object_error = quote! { #versioned_path::ParseObjectError }; + + let version_strings = &spec_gen_ctx.version_strings; + let variant_idents = &spec_gen_ctx.variant_idents; + + let enum_ident_string = spec_gen_ctx.kubernetes_idents.kind.to_string(); + + let api_versions = version_strings.iter().map(|version| { + format!( + "{group}/{version}", + group = &spec_gen_ctx.kubernetes_arguments.group + ) + }); + + Some(quote! { + fn from_json_value(object_value: #serde_json_path::Value) -> ::std::result::Result { + let kind = object_value + .get("kind") + .ok_or_else(|| #parse_object_error::FieldNotPresent { + field: "kind".to_owned() + })? + .as_str() + .ok_or_else(|| #parse_object_error::FieldNotStr { + field: "kind".to_owned() + })?; + + if kind == #enum_ident_string { + return Err(#parse_object_error::UnexpectedKind{ + kind: kind.to_owned(), + expected: #enum_ident_string.to_owned(), + }); + } + + let api_version = object_value + .get("apiVersion") + .ok_or_else(|| #parse_object_error::FieldNotPresent { + field: "apiVersion".to_owned() + })? + .as_str() + .ok_or_else(|| #parse_object_error::FieldNotStr { + field: "apiVersion".to_owned() + })?; + + let object = match api_version { + #(#api_versions => { + let object = #serde_json_path::from_value(object_value) + .map_err(|source| #parse_object_error::Deserialize { source })?; + + Self::#variant_idents(object) + },)* + unknown_api_version => return ::std::result::Result::Err(#parse_object_error::UnknownApiVersion { + api_version: unknown_api_version.to_owned() + }), + }; + + ::std::result::Result::Ok(object) + } + }) + } + + pub(super) fn generate_into_json_value_fn( + &self, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> Option { + let variant_data_ident = &spec_gen_ctx.kubernetes_idents.parameter; + let variant_idents = &spec_gen_ctx.variant_idents; + + let serde_json_path = &*mod_gen_ctx.crates.serde_json; + + Some(quote! { + fn into_json_value(self) -> ::std::result::Result<#serde_json_path::Value, #serde_json_path::Error> { + match self { + #(Self::#variant_idents(#variant_data_ident) => Ok(#serde_json_path::to_value(#variant_data_ident)?),)* + } + } + }) + } + + fn generate_conversion_match_arms( + &self, + versions: &[VersionDefinition], + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> Vec { + let variant_data_ident = &spec_gen_ctx.kubernetes_idents.parameter; + let version_enum_ident = &spec_gen_ctx.kubernetes_idents.version; + let struct_ident = &spec_gen_ctx.kubernetes_idents.kind; + + let versioned_path = &*mod_gen_ctx.crates.versioned; + let convert_object_error = quote! { #versioned_path::ConvertObjectError }; + + let conversion_paths = conversion_paths(versions); + + conversion_paths + .iter() + .map(|(start, path)| { + let current_object_version_ident = &start.idents.variant; + let current_object_version_string = &start.inner.to_string(); + + let desired_object_version = path.last().expect("the path always contains at least one element"); + let desired_object_version_string = desired_object_version.inner.to_string(); + let desired_object_variant_ident = &desired_object_version.idents.variant; + + let conversions = path.iter().enumerate().map(|(i, v)| { + let module_ident = &v.idents.module; + + if i == 0 { + quote! { + // let converted: #module_ident::#spec_ident = #variant_data_ident.spec.into(); + let converted: #module_ident::#struct_ident = #variant_data_ident.into(); + } + } else { + quote! { + // let converted: #module_ident::#spec_ident = converted.into(); + let converted: #module_ident::#struct_ident = converted.into(); + } + } + }); + + let kind = spec_gen_ctx.kubernetes_idents.kind.to_string(); + let steps = path.len(); + + let convert_object_trace = mod_gen_ctx.kubernetes_options.enable_tracing.is_present().then(|| quote! { + ::tracing::trace!( + #DESIRED_API_VERSION_ATTRIBUTE = #desired_object_version_string, + #API_VERSION_ATTRIBUTE = #current_object_version_string, + #STEPS_ATTRIBUTE = #steps, + #KIND_ATTRIBUTE = #kind, + "Successfully converted object" + ); + }); + + + quote! { + (Self::#current_object_version_ident(#variant_data_ident), #version_enum_ident::#desired_object_variant_ident) => { + #(#conversions)* + + let desired_object = Self::#desired_object_variant_ident(converted); + + let desired_object = desired_object.into_json_value() + .map_err(|source| #convert_object_error::Serialize { source })?; + + #convert_object_trace + + converted_objects.push(desired_object); + } + } + }) + .collect() + } + + fn generate_conversion_tracing( + &self, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> TracingTokens { + if mod_gen_ctx.kubernetes_options.enable_tracing.is_present() { + // TODO (@Techassi): Make tracing path configurable. Currently not possible, needs + // upstream change + let kind = spec_gen_ctx.kubernetes_idents.kind.to_string(); + + let successful_conversion_response_event = Some(quote! { + ::tracing::debug!( + #CONVERTED_OBJECT_COUNT_ATTRIBUTE = converted_objects.len(), + #KIND_ATTRIBUTE = #kind, + "Successfully converted objects" + ); + }); + + let convert_objects_instrumentation = Some(quote! { + #[::tracing::instrument( + skip_all, + err + )] + }); + + let invalid_conversion_review_event = Some(quote! { + ::tracing::warn!(?err, "received invalid conversion review"); + }); + + // NOTE (@Techassi): We sadly cannot use the constants here, because + // the fields only accept idents, which strings are not. + let try_convert_instrumentation = Some(quote! { + #[::tracing::instrument( + skip_all, + fields( + k8s.crd.conversion.api_version = review.types.api_version, + k8s.crd.conversion.kind = review.types.kind, + ) + )] + }); + + TracingTokens { + successful_conversion_response_event, + convert_objects_instrumentation, + invalid_conversion_review_event, + try_convert_instrumentation, + } + } else { + TracingTokens::default() + } + } +} + +fn conversion_paths(elements: &[T]) -> Vec<(&T, Cow<'_, [T]>)> +where + T: Clone + Ord, +{ + let mut chain = Vec::new(); + + // First, create all 2-permutations of the provided list of elements. It is important + // we select permutations instead of combinations because the order of elements matter. + // A quick example of what the iterator adaptor produces: A list with three elements + // 'v1alpha1', 'v1beta1', and 'v1' will produce six (3! / (3 - 2)!) permutations: + // + // - v1alpha1 -> v1beta1 + // - v1alpha1 -> v1 + // - v1beta1 -> v1 + // - v1beta1 -> v1alpha1 + // - v1 -> v1alpha1 + // - v1 -> v1beta1 + + for pair in elements.iter().permutations(2) { + let start = pair[0]; + let end = pair[1]; + + // Next, we select the positions of the start and end element in the original + // slice. These indices are used to construct the conversion path, which contains + // elements between start (excluding) and the end (including). These elements + // describe the steps needed to go from the start to the end (upgrade or downgrade + // depending on the direction). + if let (Some(start_index), Some(end_index)) = ( + elements.iter().position(|v| v == start), + elements.iter().position(|v| v == end), + ) { + let path = match start_index.cmp(&end_index) { + Ordering::Less => { + // If the start index is smaller than the end index (upgrade), we can return + // a slice pointing directly into the original slice. That's why Cow::Borrowed + // can be used here. + Cow::Borrowed(&elements[start_index + 1..=end_index]) + } + Ordering::Greater => { + // If the start index is bigger than the end index (downgrade), we need to reverse + // the elements. With a slice, this is only possible to do in place, which is not + // what we want in this case. Instead, the data is reversed and cloned and collected + // into a Vec and Cow::Owned is used. + let path = elements[end_index..start_index] + .iter() + .rev() + .cloned() + .collect(); + Cow::Owned(path) + } + Ordering::Equal => unreachable!( + "start and end index cannot be the same due to selecting permutations" + ), + }; + + chain.push((start, path)); + } + } + + chain +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_path_is_the_goal() { + let paths = conversion_paths(&["v1alpha1", "v1alpha2", "v1beta1", "v1"]); + assert_eq!(paths.len(), 12); + + let expected = vec![ + ("v1alpha1", vec!["v1alpha2"]), + ("v1alpha1", vec!["v1alpha2", "v1beta1"]), + ("v1alpha1", vec!["v1alpha2", "v1beta1", "v1"]), + ("v1alpha2", vec!["v1alpha1"]), + ("v1alpha2", vec!["v1beta1"]), + ("v1alpha2", vec!["v1beta1", "v1"]), + ("v1beta1", vec!["v1alpha2", "v1alpha1"]), + ("v1beta1", vec!["v1alpha2"]), + ("v1beta1", vec!["v1"]), + ("v1", vec!["v1beta1", "v1alpha2", "v1alpha1"]), + ("v1", vec!["v1beta1", "v1alpha2"]), + ("v1", vec!["v1beta1"]), + ]; + + for (result, expected) in paths.iter().zip(expected) { + assert_eq!(*result.0, expected.0); + assert_eq!(result.1.to_vec(), expected.1); + } + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/merge.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/merge.rs new file mode 100644 index 000000000..e11c71920 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/merge.rs @@ -0,0 +1,40 @@ +use proc_macro2::TokenStream; +use quote::quote; + +use crate::codegen::container::{ + ModuleGenerationContext, + r#struct::{SpecGenerationContext, Struct}, +}; + +impl Struct { + pub(super) fn generate_merged_crd_fn( + &self, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> Option { + if mod_gen_ctx.skip.merged_crd.is_present() || self.common.options.skip_merged_crd { + return None; + } + + // Get various idents needed for code generation + let version_enum_ident = &spec_gen_ctx.kubernetes_idents.version; + + // Get the crate paths + let k8s_openapi_path = &*mod_gen_ctx.crates.k8s_openapi; + let kube_core_path = &*mod_gen_ctx.crates.kube_core; + + let crd_fns = &spec_gen_ctx.crd_fns; + + Some(quote! { + /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. + pub fn merged_crd( + stored_apiversion: #version_enum_ident + ) -> ::std::result::Result< + #k8s_openapi_path::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, + #kube_core_path::crd::MergeError> + { + #kube_core_path::crd::merge_crds(vec![#(#crd_fns),*], stored_apiversion.as_version_str()) + } + }) + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs index 55f01f3ab..b28e8ed68 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs @@ -1,21 +1,24 @@ -use std::ops::Not; - -use darling::{Error, FromAttributes, Result}; +use darling::{Error, FromAttributes, Result, util::IdentString}; +use itertools::Itertools; use proc_macro2::TokenStream; use quote::quote; use syn::{Generics, ItemStruct}; use crate::{ - attrs::container::NestedContainerAttributes, + attrs::container::{ContainerAttributes, StructCrdArguments}, codegen::{ - ItemStatus, StandaloneContainerAttributes, VersionDefinition, + Direction, VersionContext, VersionDefinition, changes::Neighbors, - container::{CommonContainerData, Container, ContainerIdents, ContainerOptions, Direction}, - item::VersionedField, + container::{ + CommonContainerData, Container, ContainerIdents, ContainerOptions, ContainerTokens, + ExtendContainerTokens as _, KubernetesIdents, ModuleGenerationContext, + }, + item::{ItemStatus, VersionedField}, }, }; -mod k8s; +mod conversion; +mod merge; impl Container { pub fn new_struct(item_struct: ItemStruct, versions: &[VersionDefinition]) -> Result { @@ -28,20 +31,29 @@ impl Container { versioned_fields.push(versioned_field); } - let kubernetes_arguments = attributes.kubernetes_arguments; - let idents = ContainerIdents::from(item_struct.ident, kubernetes_arguments.as_ref()); + let idents = ContainerIdents::from(item_struct.ident); + + let kubernetes_data = attributes.crd_arguments.map(|arguments| { + let idents = KubernetesIdents::from(&idents.original, &arguments); + KubernetesData { + kubernetes_arguments: arguments, + kubernetes_idents: idents, + } + }); // Validate K8s specific requirements // Ensure that the struct name includes the 'Spec' suffix. - if kubernetes_arguments.is_some() && !idents.original.as_str().ends_with("Spec") { + if kubernetes_data.is_some() && !idents.original.as_str().ends_with("Spec") { return Err(Error::custom( "struct name needs to include the `Spec` suffix if Kubernetes features are enabled via `#[versioned(k8s())]`" ).with_span(&idents.original.span())); } let options = ContainerOptions { - skip_from: attributes.options.skip.is_some_and(|s| s.from.is_present()), - kubernetes_arguments, + skip_from: attributes.skip.from.is_present(), + skip_object_from: attributes.skip.object_from.is_present(), + skip_merged_crd: attributes.skip.merged_crd.is_present(), + skip_try_convert: attributes.skip.try_convert.is_present(), }; // Nested structs @@ -62,6 +74,7 @@ impl Container { Ok(Self::Struct(Struct { generics: item_struct.generics, + kubernetes_data, fields: versioned_fields, common, })) @@ -90,22 +103,96 @@ pub struct KubernetesData { // Common token generation impl Struct { + pub fn generate_tokens<'a>( + &self, + versions: &'a [VersionDefinition], + mod_gen_ctx: ModuleGenerationContext<'_>, + ) -> ContainerTokens<'a> { + let mut versions_iter = versions.iter().peekable(); + let mut container_tokens = ContainerTokens::default(); + + let spec_gen_ctx = + SpecGenerationContext::new(self.kubernetes_data.as_ref(), versions, mod_gen_ctx); + + while let Some(version) = versions_iter.next() { + let next_version = versions_iter.peek().copied(); + let ver_ctx = VersionContext::new(version, next_version); + + let struct_definition = + self.generate_definition(ver_ctx, mod_gen_ctx, spec_gen_ctx.as_ref()); + + let upgrade_from = self.generate_from_impl(Direction::Upgrade, ver_ctx, mod_gen_ctx); + let downgrade_from = + self.generate_from_impl(Direction::Downgrade, ver_ctx, mod_gen_ctx); + + // Generate code which is only needed for the top-level CRD spec + if let Some(spec_gen_ctx) = &spec_gen_ctx { + let upgrade_spec_from = self.generate_object_from_impl( + Direction::Upgrade, + ver_ctx, + mod_gen_ctx, + spec_gen_ctx, + ); + + let downgrade_spec_from = self.generate_object_from_impl( + Direction::Downgrade, + ver_ctx, + mod_gen_ctx, + spec_gen_ctx, + ); + + container_tokens + .extend_between(&version.inner, upgrade_spec_from) + .extend_between(&version.inner, downgrade_spec_from); + } + + container_tokens + .extend_inner(&version.inner, struct_definition) + .extend_between(&version.inner, upgrade_from) + .extend_between(&version.inner, downgrade_from); + } + + // Generate code which is only needed for the top-level CRD spec + if let Some(spec_gen_ctx) = spec_gen_ctx { + let entry_enum = self.generate_entry_enum(mod_gen_ctx, &spec_gen_ctx); + let entry_enum_impl = + self.generate_entry_impl_block(versions, mod_gen_ctx, &spec_gen_ctx); + let version_enum = self.generate_version_enum(mod_gen_ctx, &spec_gen_ctx); + let status_struct = self.generate_status_struct(mod_gen_ctx, &spec_gen_ctx); + + container_tokens + .extend_outer(entry_enum) + .extend_outer(entry_enum_impl) + .extend_outer(version_enum) + .extend_outer(status_struct); + } + + container_tokens + } + /// Generates code for the struct definition. - pub fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { + fn generate_definition( + &self, + ver_ctx: VersionContext<'_>, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: Option<&SpecGenerationContext<'_>>, + ) -> TokenStream { let where_clause = self.generics.where_clause.as_ref(); let type_generics = &self.generics; let original_attributes = &self.common.original_attributes; let ident = &self.common.idents.original; - let version_docs = &version.docs; + let version_docs = &ver_ctx.version.docs; - let mut fields = TokenStream::new(); - for field in &self.fields { - fields.extend(field.generate_for_container(version)); - } + let fields: TokenStream = self + .fields + .iter() + .filter_map(|field| field.generate_for_container(ver_ctx.version)) + .collect(); - // This only returns Some, if K8s features are enabled - let kube_attribute = self.generate_kube_attribute(version); + let kube_attribute = spec_gen_ctx.and_then(|spec_gen_ctx| { + self.generate_kube_attribute(ver_ctx, mod_gen_ctx, spec_gen_ctx) + }); quote! { #(#[doc = #version_docs])* @@ -117,93 +204,239 @@ impl Struct { } } - // TODO (@Techassi): Adjust doc comment - /// Generates code for the `From for NextVersion` implementation. - /// - /// The `add_attributes` parameter declares if attributes (macros) should be added to the - /// generated `From` impl block. - pub fn generate_from_impl( + fn generate_kube_attribute( &self, - direction: Direction, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - add_attributes: bool, + ver_ctx: VersionContext<'_>, + gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, ) -> Option { - if version.skip_from || self.common.options.skip_from { - return None; + // Required arguments + let group = &spec_gen_ctx.kubernetes_arguments.group; + let version = ver_ctx.version.inner.to_string(); + let kind = spec_gen_ctx + .kubernetes_arguments + .kind + .as_ref() + .map_or(spec_gen_ctx.kubernetes_idents.kind.to_string(), |kind| { + kind.clone() + }); + + // Optional arguments + let singular = spec_gen_ctx + .kubernetes_arguments + .singular + .as_ref() + .map(|s| quote! { , singular = #s }); + + let plural = spec_gen_ctx + .kubernetes_arguments + .plural + .as_ref() + .map(|p| quote! { , plural = #p }); + + let crates = gen_ctx.crates; + + let namespaced = spec_gen_ctx + .kubernetes_arguments + .namespaced + .is_present() + .then_some(quote! { , namespaced }); + + let status = match ( + gen_ctx + .kubernetes_options + .experimental_conversion_tracking + .is_present(), + &spec_gen_ctx.kubernetes_arguments.status, + ) { + (true, _) => { + let status_ident = &spec_gen_ctx.kubernetes_idents.status; + Some(quote! { , status = #status_ident }) + } + (_, Some(status_ident)) => Some(quote! { , status = #status_ident }), + (_, _) => None, + }; + + let shortnames: TokenStream = spec_gen_ctx + .kubernetes_arguments + .shortnames + .iter() + .map(|s| quote! { , shortname = #s }) + .collect(); + + Some(quote! { + // The end-developer needs to derive CustomResource and JsonSchema. + // This is because we don't know if they want to use a re-exported or renamed import. + #[kube( + // These must be comma separated (except the last) as they always exist: + group = #group, version = #version, kind = #kind + // These fields are optional, and therefore the token stream must prefix each with a comma: + #singular #plural #namespaced #crates #status #shortnames + )] + }) + } + + fn generate_entry_enum( + &self, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> TokenStream { + let enum_ident = &spec_gen_ctx.kubernetes_idents.kind; + let vis = mod_gen_ctx.vis; + + let variant_idents = &spec_gen_ctx.variant_idents; + let variant_data = &spec_gen_ctx.variant_data; + + let automatically_derived = mod_gen_ctx.automatically_derived_attr(); + + quote! { + #automatically_derived + #[derive(::core::fmt::Debug)] + #vis enum #enum_ident { + #(#variant_idents(#variant_data)),* + } } + } - next_version.map(|next_version| { - // TODO (@Techassi): Support generic types which have been removed in newer versions, - // but need to exist for older versions How do we represent that? Because the - // defined struct always represents the latest version. I guess we could generally - // advise against using generic types, but if you have to, avoid removing it in - // later versions. - let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl(); - let from_struct_ident = &self.common.idents.parameter; - let struct_ident = &self.common.idents.original; - - // Include allow(deprecated) only when this or the next version is - // deprecated. Also include it, when a field in this or the next - // version is deprecated. - let allow_attribute = (version.deprecated.is_some() - || next_version.deprecated.is_some() - || self.is_any_field_deprecated(version) - || self.is_any_field_deprecated(next_version)) - .then(|| quote! { #[allow(deprecated)] }); - - // Only add the #[automatically_derived] attribute only if this impl is used - // outside of a module (in standalone mode). - let automatically_derived = add_attributes - .not() - .then(|| quote! {#[automatically_derived]}); - - let fields = |direction: Direction| -> TokenStream { - self - .fields - .iter() - .filter_map(|f| { - f.generate_for_from_impl( - direction, - version, - next_version, - from_struct_ident, - ) - }) - .collect() + fn generate_entry_impl_block( + &self, + versions: &[VersionDefinition], + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> TokenStream { + let enum_ident = &spec_gen_ctx.kubernetes_idents.kind; + + // Only generate merged_crd associated function if not opted out + let merged_crd_fn = + if !mod_gen_ctx.skip.merged_crd.is_present() && !self.common.options.skip_merged_crd { + Some(self.generate_merged_crd_fn(mod_gen_ctx, spec_gen_ctx)) + } else { + None }; - let (fields, for_module_ident, from_module_ident) = match direction { - Direction::Upgrade => { - let from_module_ident = &version.idents.module; - let for_module_ident = &next_version.idents.module; + let try_convert_fn = self.generate_try_convert_fn(versions, mod_gen_ctx, spec_gen_ctx); + let from_json_value_fn = self.generate_from_json_value_fn(mod_gen_ctx, spec_gen_ctx); + let into_json_value_fn = self.generate_into_json_value_fn(mod_gen_ctx, spec_gen_ctx); + + let automatically_derived = mod_gen_ctx.automatically_derived_attr(); - (fields(Direction::Upgrade), for_module_ident, from_module_ident) + quote! { + #automatically_derived + impl #enum_ident { + #merged_crd_fn + #try_convert_fn + #from_json_value_fn + #into_json_value_fn + } + } + } + + fn generate_version_enum( + &self, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> Option { + if (mod_gen_ctx.skip.merged_crd.is_present() || self.common.options.skip_merged_crd) + && (mod_gen_ctx.skip.try_convert.is_present() || self.common.options.skip_try_convert) + { + return None; + } + + let enum_ident = &spec_gen_ctx.kubernetes_idents.version; + let vis = mod_gen_ctx.vis; + + let versioned_path = &*mod_gen_ctx.crates.versioned; + let unknown_desired_api_version_error = + quote! { #versioned_path::UnknownDesiredApiVersionError }; + + let version_strings = &spec_gen_ctx.version_strings; + let variant_idents = &spec_gen_ctx.variant_idents; + + let automatically_derived = mod_gen_ctx.automatically_derived_attr(); + + // TODO (@Techassi): Generate this once + let api_versions = version_strings + .iter() + .map(|version| { + format!( + "{group}/{version}", + group = &spec_gen_ctx.kubernetes_arguments.group + ) + }) + .collect::>(); + + Some(quote! { + #automatically_derived + #[derive( + ::core::marker::Copy, + ::core::clone::Clone, + ::core::fmt::Debug + )] + #vis enum #enum_ident { + #(#variant_idents),* + } + + #automatically_derived + impl ::core::fmt::Display for #enum_ident { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { + // The version (without the Kubernetes group) is probably more human-readable + f.write_str(self.as_version_str()) } - Direction::Downgrade => { - let from_module_ident = &next_version.idents.module; - let for_module_ident = &version.idents.module; + } - (fields(Direction::Downgrade), for_module_ident, from_module_ident) + #automatically_derived + impl #enum_ident { + pub fn as_version_str(&self) -> &str { + match self { + #(#enum_ident::#variant_idents => #version_strings),* + } + } + + pub fn as_api_version_str(&self) -> &str { + match self { + #(#enum_ident::#variant_idents => #api_versions),* + } } - }; - quote! { - #automatically_derived - #allow_attribute - impl #impl_generics ::std::convert::From<#from_module_ident::#struct_ident #type_generics> for #for_module_ident::#struct_ident #type_generics - #where_clause - { - fn from(#from_struct_ident: #from_module_ident::#struct_ident #type_generics) -> Self { - Self { - #fields - } + pub fn from_api_version(api_version: &str) -> Result { + match api_version { + #(#api_versions => Ok(#enum_ident::#variant_idents)),*, + _ => Err(#unknown_desired_api_version_error { + api_version: api_version.to_owned(), + }), } } } }) } + /// Generates the Kubernetes specific From impl for all structs which are part of a spec. + fn generate_from_impl( + &self, + direction: Direction, + ver_ctx: VersionContext<'_>, + mod_gen_ctx: ModuleGenerationContext<'_>, + ) -> Option { + if mod_gen_ctx.skip.from.is_present() || self.common.options.skip_from { + return None; + } + + if !mod_gen_ctx + .kubernetes_options + .experimental_conversion_tracking + .is_present() + { + return None; + } + + let next_version = ver_ctx.next_version; + let version = ver_ctx.version; + + next_version.map(|next_version| { + self.generate_tracking_from_impl(direction, version, next_version, mod_gen_ctx) + }) + } + /// Returns whether any field is deprecated in the provided `version`. fn is_any_field_deprecated(&self, version: &VersionDefinition) -> bool { // First, iterate over all fields. The `any` function will return true @@ -213,9 +446,9 @@ impl Struct { // ensure it is deprecated. self.fields.iter().any(|f| { f.changes.as_ref().is_some_and(|c| { - c.value_is(&version.inner, |a| { + c.value_is(&version.inner, |s| { matches!( - a, + s, ItemStatus::Deprecation { .. } | ItemStatus::NoChange { previously_deprecated: true, @@ -227,3 +460,65 @@ impl Struct { }) } } + +#[derive(Debug)] +pub struct SpecGenerationContext<'a> { + pub kubernetes_arguments: &'a StructCrdArguments, + pub kubernetes_idents: &'a KubernetesIdents, + + pub crd_fns: Vec, + pub variant_idents: Vec, + pub variant_data: Vec, + pub version_strings: Vec, +} + +impl<'a> SpecGenerationContext<'a> { + pub fn new( + data: Option<&'a KubernetesData>, + versions: &[VersionDefinition], + mod_gen_ctx: ModuleGenerationContext<'_>, + ) -> Option { + match data { + Some(KubernetesData { + kubernetes_arguments, + kubernetes_idents, + }) => { + let (crd_fns, variant_idents, variant_data, version_strings) = versions + .iter() + .map(|version| { + Self::generate_version_items(version, mod_gen_ctx, &kubernetes_idents.kind) + }) + .multiunzip::<(Vec<_>, Vec<_>, Vec<_>, Vec<_>)>(); + + Some(Self { + kubernetes_arguments, + kubernetes_idents, + crd_fns, + variant_idents, + variant_data, + version_strings, + }) + } + None => None, + } + } + + fn generate_version_items( + version: &VersionDefinition, + mod_gen_ctx: ModuleGenerationContext<'_>, + struct_ident: &IdentString, + ) -> (TokenStream, IdentString, TokenStream, String) { + let module_ident = &version.idents.module; + + let kube_core_path = &*mod_gen_ctx.crates.kube_core; + + let variant_data = quote! { #module_ident::#struct_ident }; + let crd_fn = quote! { + <#module_ident::#struct_ident as #kube_core_path::CustomResourceExt>::crd() + }; + let variant_ident = version.idents.variant.clone(); + let version_string = version.inner.to_string(); + + (crd_fn, variant_ident, variant_data, version_string) + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/item/field.rs b/crates/stackable-versioned-macros/src/codegen/item/field.rs index 9cfd403ba..949a4c656 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/field.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/field.rs @@ -3,15 +3,16 @@ use std::collections::BTreeMap; use darling::{FromField, Result, util::IdentString}; use k8s_version::Version; use proc_macro2::TokenStream; -use quote::quote; +use quote::{format_ident, quote}; use syn::{Attribute, Field, Type}; use crate::{ attrs::item::FieldAttributes, codegen::{ - ItemStatus, VersionDefinition, + Direction, VersionDefinition, changes::{BTreeMapExt, ChangesetExt}, - container::Direction, + item::ItemStatus, + module::ModuleGenerationContext, }, utils::FieldIdent, }; @@ -20,6 +21,7 @@ pub struct VersionedField { pub original_attributes: Vec, pub changes: Option>, pub ident: FieldIdent, + pub nested: bool, pub ty: Type, } @@ -28,20 +30,22 @@ impl VersionedField { let field_attributes = FieldAttributes::from_field(&field)?; field_attributes.validate_versions(versions)?; - let field_ident = FieldIdent::from( - field - .ident - .expect("internal error: field must have an ident"), - ); + let ident = field + .ident + .expect("internal error: field must have an ident"); + let idents = ident.into(); + let changes = field_attributes .common - .into_changeset(&field_ident, field.ty.clone()); + .into_changeset(&idents, field.ty.clone()); + let nested = field_attributes.nested.is_present(); Ok(Self { original_attributes: field_attributes.attrs, - ident: field_ident, + ident: idents, ty: field.ty, changes, + nested, }) } @@ -185,17 +189,41 @@ impl VersionedField { #to_ident: #upgrade_fn(#from_struct_ident.#from_ident), }), // Default .into() call using From impls. - None => Some(quote! { - #to_ident: #from_struct_ident.#from_ident.into(), - }), + None => { + let json_path_ident = + format_ident!("__sv_{ident}_path", ident = to_ident.as_ident()); + + if self.nested { + Some(quote! { + #to_ident: #from_struct_ident.#from_ident.tracking_into(status, #json_path_ident), + }) + } else { + Some(quote! { + #to_ident: #from_struct_ident.#from_ident.into(), + }) + } + } }, Direction::Downgrade => match downgrade_with { Some(downgrade_fn) => Some(quote! { #from_ident: #downgrade_fn(#from_struct_ident.#to_ident), }), - None => Some(quote! { - #from_ident: #from_struct_ident.#to_ident.into(), - }), + None => { + let json_path_ident = format_ident!( + "__sv_{ident}_path", + ident = from_ident.as_ident() + ); + + if self.nested { + Some(quote! { + #from_ident: #from_struct_ident.#to_ident.tracking_into(status, #json_path_ident), + }) + } else { + Some(quote! { + #from_ident: #from_struct_ident.#to_ident.into(), + }) + } + } }, }, (old, next) => { @@ -206,9 +234,22 @@ impl VersionedField { // currently not sure why it is there and if it is needed // in some edge cases. match direction { - Direction::Upgrade => Some(quote! { - #next_field_ident: #from_struct_ident.#old_field_ident.into(), - }), + Direction::Upgrade => { + let json_path_ident = format_ident!( + "__sv_{ident}_path", + ident = next_field_ident.as_ident() + ); + + if self.nested { + Some(quote! { + #next_field_ident: #from_struct_ident.#old_field_ident.tracking_into(status, #json_path_ident), + }) + } else { + Some(quote! { + #next_field_ident: #from_struct_ident.#old_field_ident.into(), + }) + } + } Direction::Downgrade => Some(quote! { #old_field_ident: #from_struct_ident.#next_field_ident.into(), }), @@ -218,11 +259,126 @@ impl VersionedField { } None => { let field_ident = &*self.ident; + let json_path_ident = + format_ident!("__sv_{ident}_path", ident = field_ident.as_ident()); + + if self.nested { + Some(quote! { + #field_ident: #from_struct_ident.#field_ident.tracking_into(status, &#json_path_ident) + }) + } else { + Some(quote! { + #field_ident: #from_struct_ident.#field_ident.into(), + }) + } + } + } + } + + pub fn generate_for_status_insertion( + &self, + direction: Direction, + next_version: &VersionDefinition, + from_struct_ident: &IdentString, + mod_gen_ctx: ModuleGenerationContext<'_>, + ) -> Option { + let changes = self.changes.as_ref()?; + + match direction { + // This arm is only relevant for removed fields which are currently + // not supported. + Direction::Upgrade => None, + + // When we generate code for a downgrade, any changes which need to + // be tracked need to be inserted into the upgrade section for the + // next time an upgrade needs to be done. + Direction::Downgrade => { + let next_change = changes.get_expect(&next_version.inner); + + let serde_yaml_path = &*mod_gen_ctx.crates.serde_yaml; + let versioned_path = &*mod_gen_ctx.crates.versioned; + + match next_change { + ItemStatus::Addition { ident, .. } => { + // TODO (@Techassi): Only do this formatting once, but that requires extensive + // changes to the field ident and changeset generation + let json_path_ident = + format_ident!("__sv_{ident}_path", ident = ident.as_ident()); + + Some(quote! { + upgrades.push(#versioned_path::ChangedValue { + field_name: #json_path_ident, + value: #serde_yaml_path::to_value(&#from_struct_ident.#ident).unwrap(), + }); + }) + } + _ => None, + } + } + } + } + + pub fn generate_for_status_removal( + &self, + direction: Direction, + next_version: &VersionDefinition, + ) -> Option { + // If there are no changes for this field, there is also no need to generate a match arm + // for applying a tracked value. + let changes = self.changes.as_ref()?; + + match direction { + Direction::Upgrade => { + let next_change = changes.get_expect(&next_version.inner); + let ident = &self.ident; + let json_path_ident = format_ident!("__sv_{}_path", &self.ident.as_ident()); + + match next_change { + ItemStatus::NotPresent | ItemStatus::NoChange { .. } => None, + _ => Some(quote! { + field_name if field_name == #json_path_ident => { + spec.#ident = serde_yaml::from_value(value).unwrap(); + }, + }), + } + } + Direction::Downgrade => None, + } + } + + pub fn generate_for_json_path(&self, next_version: &VersionDefinition) -> Option { + match (&self.changes, self.nested) { + // If there are no changes and the field also not marked as nested, there is no need to + // generate a path variable for that field as no tracked values need to be applied/inserted + // and the tracking mechanism doesn't need to be forwarded to a sub struct. + (None, false) => None, + + // If the field is marked as nested, a path variable for that field needs to be generated + // which is then passed down to the sub struct. There is however no need to look determine + // if the field itself also has changes. This is explicitly handled by the following match + // arm. + (_, true) => { + let field_ident = format_ident!("__sv_{}_path", &self.ident.as_ident()); + let child_string = &self.ident.to_string(); Some(quote! { - #field_ident: #from_struct_ident.#field_ident.into(), + let #field_ident = ::stackable_versioned::jthong_path(parent, #child_string); }) } + (Some(changes), _) => { + let next_change = changes.get_expect(&next_version.inner); + + match next_change { + ItemStatus::NoChange { .. } | ItemStatus::NotPresent => None, + _ => { + let field_ident = format_ident!("__sv_{}_path", &self.ident.as_ident()); + let child_string = &self.ident.to_string(); + Some(quote! { + let #field_ident = ::stackable_versioned::jthong_path(parent, #child_string); + }) + } + } + } } } } diff --git a/crates/stackable-versioned-macros/src/codegen/item/mod.rs b/crates/stackable-versioned-macros/src/codegen/item/mod.rs index 8c7ce2b66..6fae281e2 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/mod.rs @@ -1,5 +1,50 @@ +use darling::util::IdentString; +use syn::{Path, Type}; + mod field; pub use field::*; mod variant; pub use variant::*; + +#[derive(Debug, PartialEq)] +pub enum ItemStatus { + Addition { + ident: IdentString, + default_fn: Path, + // NOTE (@Techassi): We need to carry idents and type information in + // nearly every status. Ideally, we would store this in separate maps. + ty: Type, + }, + Change { + downgrade_with: Option, + upgrade_with: Option, + from_ident: IdentString, + to_ident: IdentString, + from_type: Type, + to_type: Type, + }, + Deprecation { + previous_ident: IdentString, + note: Option, + ident: IdentString, + }, + NoChange { + previously_deprecated: bool, + ident: IdentString, + ty: Type, + }, + NotPresent, +} + +impl ItemStatus { + pub fn get_ident(&self) -> &IdentString { + match &self { + ItemStatus::Addition { ident, .. } => ident, + ItemStatus::Change { to_ident, .. } => to_ident, + ItemStatus::Deprecation { ident, .. } => ident, + ItemStatus::NoChange { ident, .. } => ident, + ItemStatus::NotPresent => unreachable!("ItemStatus::NotPresent does not have an ident"), + } + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/item/variant.rs b/crates/stackable-versioned-macros/src/codegen/item/variant.rs index 30dd188d8..591bf26e2 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/variant.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/variant.rs @@ -9,9 +9,9 @@ use syn::{Attribute, Fields, Type, TypeNever, Variant, token::Not}; use crate::{ attrs::item::VariantAttributes, codegen::{ - ItemStatus, VersionDefinition, + Direction, VersionDefinition, changes::{BTreeMapExt, ChangesetExt}, - container::Direction, + item::ItemStatus, }, utils::VariantIdent, }; diff --git a/crates/stackable-versioned-macros/src/codegen/mod.rs b/crates/stackable-versioned-macros/src/codegen/mod.rs index 4dd91163c..1e1117876 100644 --- a/crates/stackable-versioned-macros/src/codegen/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/mod.rs @@ -1,7 +1,5 @@ use darling::util::IdentString; use k8s_version::Version; -use proc_macro2::TokenStream; -use syn::{Path, Type}; use crate::{ attrs::module::ModuleAttributes, @@ -72,63 +70,27 @@ pub struct VersionIdents { pub variant: IdentString, } -#[derive(Debug, PartialEq)] -pub enum ItemStatus { - Addition { - ident: IdentString, - default_fn: Path, - // NOTE (@Techassi): We need to carry idents and type information in - // nearly every status. Ideally, we would store this in separate maps. - ty: Type, - }, - Change { - downgrade_with: Option, - upgrade_with: Option, - from_ident: IdentString, - to_ident: IdentString, - from_type: Type, - to_type: Type, - }, - Deprecation { - previous_ident: IdentString, - note: Option, - ident: IdentString, - }, - NoChange { - previously_deprecated: bool, - ident: IdentString, - ty: Type, - }, - NotPresent, +#[derive(Clone, Copy, Debug)] +pub struct VersionContext<'a> { + pub version: &'a VersionDefinition, + pub next_version: Option<&'a VersionDefinition>, } -impl ItemStatus { - pub fn get_ident(&self) -> &IdentString { - match &self { - ItemStatus::Addition { ident, .. } => ident, - ItemStatus::Change { to_ident, .. } => to_ident, - ItemStatus::Deprecation { ident, .. } => ident, - ItemStatus::NoChange { ident, .. } => ident, - ItemStatus::NotPresent => unreachable!("ItemStatus::NotPresent does not have an ident"), +impl<'a> VersionContext<'a> { + pub fn new( + version: &'a VersionDefinition, + next_version: Option<&'a VersionDefinition>, + ) -> Self { + Self { + version, + next_version, } } } -// This contains all generated Kubernetes tokens for a particular version. -// This struct can then be used to fully generate the combined final Kubernetes code. -#[derive(Debug, Default)] -pub struct KubernetesTokens { - variant_idents: Vec, - variant_data: Vec, - variant_strings: Vec, - crd_fns: Vec, -} - -impl KubernetesTokens { - pub fn push(&mut self, items: (TokenStream, IdentString, TokenStream, String)) { - self.crd_fns.push(items.0); - self.variant_idents.push(items.1); - self.variant_data.push(items.2); - self.variant_strings.push(items.3); - } +/// Describes the direction of [`From`] implementations. +#[derive(Copy, Clone, Debug)] +pub enum Direction { + Upgrade, + Downgrade, } diff --git a/crates/stackable-versioned-macros/src/codegen/module.rs b/crates/stackable-versioned-macros/src/codegen/module.rs index 9fcfa863b..779653690 100644 --- a/crates/stackable-versioned-macros/src/codegen/module.rs +++ b/crates/stackable-versioned-macros/src/codegen/module.rs @@ -3,13 +3,16 @@ use std::{collections::HashMap, ops::Not}; use darling::{Error, Result, util::IdentString}; use proc_macro2::TokenStream; use quote::quote; -use syn::{Ident, Item, ItemMod, ItemUse, Visibility, token::Pub}; +use syn::{Item, ItemMod, ItemUse, Visibility, token::Pub}; use crate::{ - ModuleAttributes, + attrs::module::{ + CrateArguments, KubernetesConfigOptions, ModuleAttributes, ModuleOptions, + ModuleSkipArguments, + }, codegen::{ - KubernetesTokens, VersionDefinition, - container::{Container, Direction}, + VersionDefinition, + container::{Container, ContainerTokens, VersionedContainerTokens}, }, }; @@ -27,9 +30,9 @@ pub struct Module { ident: IdentString, vis: Visibility, - // Flags which influence generation - preserve_module: bool, - skip_from: bool, + crates: CrateArguments, + options: ModuleOptions, + skip: ModuleSkipArguments, } impl Module { @@ -43,19 +46,6 @@ impl Module { let versions: Vec = (&module_attributes).into(); - let preserve_module = module_attributes - .common - .options - .preserve_module - .is_present(); - - let skip_from = module_attributes - .common - .options - .skip - .as_ref() - .is_some_and(|opts| opts.from.is_present()); - let mut errors = Error::accumulator(); let mut submodules = HashMap::new(); let mut containers = Vec::new(); @@ -64,14 +54,14 @@ impl Module { match item { Item::Enum(item_enum) => { if let Some(container) = - errors.handle(Container::new_enum_nested(item_enum, &versions)) + errors.handle(Container::new_enum(item_enum, &versions)) { containers.push(container); }; } Item::Struct(item_struct) => { if let Some(container) = - errors.handle(Container::new_struct_nested(item_struct, &versions)) + errors.handle(Container::new_struct(item_struct, &versions)) { containers.push(container); } @@ -121,7 +111,7 @@ impl Module { // defined in the module are no longer accessible (because they are not re-emitted). disallowed_item => errors.push( Error::custom( - "Item not allowed here. Please move it ouside of the versioned module", + "Item not allowed here. Please move it outside of the versioned module", ) .with_span(&disallowed_item), ), @@ -129,12 +119,13 @@ impl Module { } errors.finish_with(Self { + options: module_attributes.options, + crates: module_attributes.crates, + skip: module_attributes.skip, ident: item_mod.ident.into(), vis: item_mod.vis, - preserve_module, containers, submodules, - skip_from, versions, }) } @@ -145,6 +136,9 @@ impl Module { return quote! {}; } + let preserve_module = self.options.common.preserve_module.is_present(); + let allow_unsorted = self.options.common.allow_unsorted.is_present(); + let module_ident = &self.ident; let module_vis = &self.vis; @@ -152,63 +146,54 @@ impl Module { // of version modules (eg. 'v1alpha1') to be public, so that they are accessible inside the // preserved (wrapping) module. Otherwise, we can inherit the visibility from the module // which will be erased. - let version_module_vis = if self.preserve_module { + let version_module_vis = if allow_unsorted { &Visibility::Public(Pub::default()) } else { &self.vis }; - let mut kubernetes_tokens = TokenStream::new(); + let mut inner_and_between_tokens = HashMap::new(); + let mut outer_tokens = TokenStream::new(); let mut tokens = TokenStream::new(); - let mut kubernetes_container_items: HashMap = HashMap::new(); - let mut versions = self.versions.iter().peekable(); + let ctx = ModuleGenerationContext { + kubernetes_options: &self.options.kubernetes, + add_attributes: preserve_module, + vis: version_module_vis, + crates: &self.crates, + skip: &self.skip, + }; - while let Some(version) = versions.next() { - let next_version = versions.peek().copied(); - let mut container_definitions = TokenStream::new(); - let mut from_impls = TokenStream::new(); + for container in &self.containers { + let ContainerTokens { versioned, outer } = + container.generate_tokens(&self.versions, ctx); - let version_module_ident = &version.idents.module; + inner_and_between_tokens.insert(container.get_original_ident(), versioned); + outer_tokens.extend(outer); + } - for container in &self.containers { - container_definitions.extend(container.generate_definition(version)); - - if !self.skip_from { - from_impls.extend(container.generate_from_impl( - Direction::Upgrade, - version, - next_version, - self.preserve_module, - )); - - from_impls.extend(container.generate_from_impl( - Direction::Downgrade, - version, - next_version, - self.preserve_module, - )); - } + // Only add #[automatically_derived] here if the user doesn't want to preserve the + // module. + let automatically_derived = preserve_module + .not() + .then(|| quote! {#[automatically_derived]}); - // Generate Kubernetes specific code which is placed outside of the container - // definition. - if let Some(items) = container.generate_kubernetes_version_items(version) { - let entry = kubernetes_container_items - .entry(container.get_original_ident().clone()) - .or_default(); + for version in &self.versions { + let mut inner_tokens = TokenStream::new(); + let mut between_tokens = TokenStream::new(); - entry.push(items); - } + for container in &self.containers { + let versioned = inner_and_between_tokens + .get_mut(container.get_original_ident()) + .unwrap(); + let VersionedContainerTokens { inner, between } = + versioned.remove(&version.inner).unwrap(); + + inner_tokens.extend(inner); + between_tokens.extend(between); } - let submodule_imports = self.generate_submodule_imports(version); - - // Only add #[automatically_derived] here if the user doesn't want to preserve the - // module. - let automatically_derived = self - .preserve_module - .not() - .then(|| quote! {#[automatically_derived]}); + let version_module_ident = &version.idents.module; // Add the #[deprecated] attribute when the version is marked as deprecated. let deprecated_attribute = version @@ -216,6 +201,8 @@ impl Module { .as_ref() .map(|note| quote! { #[deprecated = #note] }); + let submodule_imports = self.generate_submodule_imports(version); + tokens.extend(quote! { #automatically_derived #deprecated_attribute @@ -223,39 +210,25 @@ impl Module { use super::*; #submodule_imports - - #container_definitions + #inner_tokens } - #from_impls + #between_tokens }); } - // Generate the final Kubernetes specific code for each container (which uses Kubernetes - // specific features) which is appended to the end of container definitions. - for container in &self.containers { - if let Some(items) = kubernetes_container_items.get(container.get_original_ident()) { - kubernetes_tokens.extend(container.generate_kubernetes_code( - &self.versions, - items, - version_module_vis, - self.preserve_module, - )); - } - } - - if self.preserve_module { + if preserve_module { quote! { #[automatically_derived] #module_vis mod #module_ident { #tokens - #kubernetes_tokens + #outer_tokens } } } else { quote! { #tokens - #kubernetes_tokens + #outer_tokens } } } @@ -272,3 +245,21 @@ impl Module { }) } } + +#[derive(Clone, Copy, Debug)] +pub struct ModuleGenerationContext<'a> { + pub kubernetes_options: &'a KubernetesConfigOptions, + pub skip: &'a ModuleSkipArguments, + pub crates: &'a CrateArguments, + pub vis: &'a Visibility, + + pub add_attributes: bool, +} + +impl ModuleGenerationContext<'_> { + pub fn automatically_derived_attr(&self) -> Option { + self.add_attributes + .not() + .then(|| quote! { #[automatically_derived] }) + } +} diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs index 2474926e9..0783a4429 100644 --- a/crates/stackable-versioned-macros/src/lib.rs +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -2,10 +2,7 @@ use darling::{FromMeta, ast::NestedMeta}; use proc_macro::TokenStream; use syn::{Error, Item, spanned::Spanned}; -use crate::{ - attrs::{container::StandaloneContainerAttributes, module::ModuleAttributes}, - codegen::{container::StandaloneContainer, module::Module}, -}; +use crate::{attrs::module::ModuleAttributes, codegen::module::Module}; #[cfg(test)] mod test_utils; @@ -1014,39 +1011,9 @@ fn versioned_impl(attrs: proc_macro2::TokenStream, input: Item) -> proc_macro2:: module.generate_tokens() } - Item::Enum(item_enum) => { - let container_attributes: StandaloneContainerAttributes = - match parse_outer_attributes(attrs) { - Ok(ca) => ca, - Err(err) => return err.write_errors(), - }; - - let standalone_enum = - match StandaloneContainer::new_enum(item_enum, container_attributes) { - Ok(standalone_enum) => standalone_enum, - Err(err) => return err.write_errors(), - }; - - standalone_enum.generate_tokens() - } - Item::Struct(item_struct) => { - let container_attributes: StandaloneContainerAttributes = - match parse_outer_attributes(attrs) { - Ok(ca) => ca, - Err(err) => return err.write_errors(), - }; - - let standalone_struct = - match StandaloneContainer::new_struct(item_struct, container_attributes) { - Ok(standalone_struct) => standalone_struct, - Err(err) => return err.write_errors(), - }; - - standalone_struct.generate_tokens() - } _ => Error::new( input.span(), - "attribute macro `versioned` can be only be applied to modules, structs and enums", + "attribute macro `versioned` can be only be applied to modules", ) .into_compile_error(), } @@ -1066,6 +1033,7 @@ mod snapshot_tests { use super::*; + // TODO (@Techassi): Combine tests, there are no default/k8s-specific tests anymore #[test] fn default() { let _settings_guard = test_utils::set_snapshot_path().bind_to_scope(); @@ -1078,7 +1046,6 @@ mod snapshot_tests { }); } - #[cfg(feature = "k8s")] #[test] fn k8s() { let _settings_guard = test_utils::set_snapshot_path().bind_to_scope(); diff --git a/crates/stackable-versioned/src/lib.rs b/crates/stackable-versioned/src/lib.rs index 5a768b19b..8c36ef00f 100644 --- a/crates/stackable-versioned/src/lib.rs +++ b/crates/stackable-versioned/src/lib.rs @@ -11,7 +11,6 @@ use std::collections::HashMap; -use k8s_version::Version; use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec}; use snafu::{ErrorCompat, Snafu}; // Re-export @@ -26,11 +25,8 @@ where Self: Sized, S: TrackingStatus + Default, { - /// Describes the parent field of `Self`. None indicates that `Self` is at the root. - const PARENT: Option<&str>; - /// Convert `T` into `Self`. - fn tracking_from(value: T, status: &mut S) -> Self; + fn tracking_from(value: T, status: &mut S, parent: &str) -> Self; } /// A value-to-value conversion that consumes the input value while tracking changes via a @@ -44,7 +40,7 @@ where S: TrackingStatus + Default, { /// Convert `Self` into `T`. - fn tracking_into(self, status: &mut S) -> T; + fn tracking_into(self, status: &mut S, parent: &str) -> T; } impl TrackingInto for T @@ -52,8 +48,8 @@ where S: TrackingStatus + Default, U: TrackingFrom, { - fn tracking_into(self, status: &mut S) -> U { - U::tracking_from(self, status) + fn tracking_into(self, status: &mut S, parent: &str) -> U { + U::tracking_from(self, status, parent) } } @@ -70,10 +66,10 @@ pub trait TrackingStatus { #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] pub struct ChangedValues { /// List of values needed when downgrading to a particular version. - pub downgrades: HashMap>, + pub downgrades: HashMap>, /// List of values needed when upgrading to a particular version. - pub upgrades: HashMap>, + pub upgrades: HashMap>, // TODO (@Techassi): Add a version indicator here if we ever decide to change the tracking // mechanism. } @@ -108,17 +104,20 @@ fn raw_object_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema { /// This error indicates that parsing an object from a conversion review failed. #[derive(Debug, Snafu)] pub enum ParseObjectError { - #[snafu(display(r#"failed to find "apiVersion" field"#))] - FieldNotPresent, + #[snafu(display("the field {field:?} is missing"))] + FieldNotPresent { field: String }, - #[snafu(display(r#"the "apiVersion" field must be a string"#))] - FieldNotStr, + #[snafu(display("the field {field:?} must be a string"))] + FieldNotStr { field: String }, #[snafu(display("encountered unknown object API version {api_version:?}"))] UnknownApiVersion { api_version: String }, #[snafu(display("failed to deserialize object from JSON"))] Deserialize { source: serde_json::Error }, + + #[snafu(display("unexpected object kind {kind:?}, expected {expected:?}"))] + UnexpectedKind { kind: String, expected: String }, } /// This error indicates that converting an object from a conversion review to the desired @@ -130,6 +129,11 @@ pub enum ConvertObjectError { #[snafu(display("failed to serialize object into json"))] Serialize { source: serde_json::Error }, + + #[snafu(display("failed to parse desired API version"))] + ParseDesiredApiVersion { + source: UnknownDesiredApiVersionError, + }, } impl ConvertObjectError { @@ -150,6 +154,24 @@ impl ConvertObjectError { match self { ConvertObjectError::Parse { .. } => 400, ConvertObjectError::Serialize { .. } => 500, + + // This is likely the clients fault, as it is requesting a unsupported version + ConvertObjectError::ParseDesiredApiVersion { + source: UnknownDesiredApiVersionError { .. }, + } => 400, } } } + +#[derive(Debug, Snafu)] +#[snafu(display("unknown API version {api_version:?}"))] +pub struct UnknownDesiredApiVersionError { + pub api_version: String, +} + +pub fn jthong_path(parent: &str, child: &str) -> String { + match parent.is_empty() { + true => child.to_owned(), + false => format!("{parent}.{child}"), + } +} From 19017f2a250622c097b01084dea8f7c0f56acd79 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 4 Jul 2025 09:52:31 +0200 Subject: [PATCH 07/16] fix(stackable-versioned): Fix edge-cases uncovered by UI tests --- .../codegen/container/struct/conversion.rs | 83 +++++++------ .../src/codegen/container/struct/mod.rs | 117 ++++++++++++++---- .../src/codegen/item/field.rs | 30 +++-- 3 files changed, 154 insertions(+), 76 deletions(-) diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs index 9229492a9..524951de2 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, cmp::Ordering, ops::Not as _}; +use std::{borrow::Cow, cmp::Ordering}; use indoc::formatdoc; use itertools::Itertools as _; @@ -44,14 +44,6 @@ impl Struct { return None; } - if !mod_gen_ctx - .kubernetes_options - .experimental_conversion_tracking - .is_present() - { - return None; - } - let next_version = ver_ctx.next_version; let version = ver_ctx.version; @@ -69,29 +61,49 @@ impl Struct { Direction::Downgrade => (&version.idents.module, &next_version.idents.module), }; + // NOTE (@Techassi): This if statement can be removed once experimental_conversion_tracking + // is gone. + let from_inner = if mod_gen_ctx.kubernetes_options.experimental_conversion_tracking.is_present() { + quote! { + // The status is optional. The be able to track changes in nested sub structs it needs + // to be initialized with a default value. + let mut status = #from_struct_parameter_ident.status.unwrap_or_default(); + + // Convert the spec and track values in the status + let spec = + <#for_module_ident::#spec_struct_ident as #versioned_path::TrackingFrom<_, _>>::tracking_from( + #from_struct_parameter_ident.spec, + &mut status, + "", + ); + + // Construct the final object by copying over the metadata, setting the status and + // using the converted spec. + Self { + metadata: #from_struct_parameter_ident.metadata, + status: Some(status), + spec, + } + } + } else { + let status = spec_gen_ctx.kubernetes_arguments.status + .as_ref() + .map(|_| quote! { status: #from_struct_parameter_ident.status,}); + + quote! { + Self { + metadata: #from_struct_parameter_ident.metadata, + spec: #from_struct_parameter_ident.spec.into(), + #status + } + } + }; + quote! { #automatically_derived impl ::std::convert::From<#from_module_ident::#object_struct_ident> for #for_module_ident::#object_struct_ident { fn from(#from_struct_parameter_ident: #from_module_ident::#object_struct_ident) -> Self { - // The status is optional. The be able to track changes in nested sub structs it needs - // to be initialized with a default value. - let mut status = #from_struct_parameter_ident.status.unwrap_or_default(); - - // Convert the spec and track values in the status - let spec = - <#for_module_ident::#spec_struct_ident as #versioned_path::TrackingFrom<_, _>>::tracking_from( - #from_struct_parameter_ident.spec, - &mut status, - "", - ); - - // Construct the final object by copying over the metadata, setting the status and - // using the converted spec. - Self { - metadata: #from_struct_parameter_ident.metadata, - status: Some(status), - spec, - } + #from_inner } } } @@ -126,10 +138,7 @@ impl Struct { // Only add the #[automatically_derived] attribute only if this impl is used // outside of a module (in standalone mode). - let automatically_derived = mod_gen_ctx - .add_attributes - .not() - .then(|| quote! {#[automatically_derived]}); + let automatically_derived = mod_gen_ctx.automatically_derived_attr(); let fields = |direction: Direction| -> TokenStream { self.fields @@ -169,8 +178,8 @@ impl Struct { where S: #versioned_path::TrackingStatus + ::core::default::Default { - #[allow(unused)] fn tracking_from(#from_struct_ident: #from_module_ident::#struct_ident, status: &mut S, parent: &str) -> Self { + // TODO (@Techassi): Only emit this if any of the fields below need it use #versioned_path::TrackingInto as _; #json_paths @@ -275,10 +284,6 @@ impl Struct { } fn generate_json_paths(&self, next_version: &VersionDefinition) -> Option { - // if !self.needs_tracking(next_version) { - // return None; - // } - let json_paths = self .fields .iter() @@ -585,6 +590,10 @@ impl Struct { mod_gen_ctx: ModuleGenerationContext<'_>, spec_gen_ctx: &SpecGenerationContext<'_>, ) -> Option { + if mod_gen_ctx.skip.try_convert.is_present() || self.common.options.skip_try_convert { + return None; + } + let variant_data_ident = &spec_gen_ctx.kubernetes_idents.parameter; let variant_idents = &spec_gen_ctx.variant_idents; diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs index b28e8ed68..4b891ca2a 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs @@ -45,7 +45,7 @@ impl Container { // Ensure that the struct name includes the 'Spec' suffix. if kubernetes_data.is_some() && !idents.original.as_str().ends_with("Spec") { return Err(Error::custom( - "struct name needs to include the `Spec` suffix if Kubernetes features are enabled via `#[versioned(k8s())]`" + "struct name needs to include the `Spec` suffix if CRD features are enabled via `#[versioned(crd())]`" ).with_span(&idents.original.span())); } @@ -207,7 +207,7 @@ impl Struct { fn generate_kube_attribute( &self, ver_ctx: VersionContext<'_>, - gen_ctx: ModuleGenerationContext<'_>, + mod_gen_ctx: ModuleGenerationContext<'_>, spec_gen_ctx: &SpecGenerationContext<'_>, ) -> Option { // Required arguments @@ -234,7 +234,7 @@ impl Struct { .as_ref() .map(|p| quote! { , plural = #p }); - let crates = gen_ctx.crates; + let crates = mod_gen_ctx.crates; let namespaced = spec_gen_ctx .kubernetes_arguments @@ -242,19 +242,29 @@ impl Struct { .is_present() .then_some(quote! { , namespaced }); + // NOTE (@Techassi): What an abomination let status = match ( - gen_ctx + mod_gen_ctx .kubernetes_options .experimental_conversion_tracking .is_present(), &spec_gen_ctx.kubernetes_arguments.status, ) { - (true, _) => { - let status_ident = &spec_gen_ctx.kubernetes_idents.status; - Some(quote! { , status = #status_ident }) + (true, status_path) => { + if (mod_gen_ctx.skip.merged_crd.is_present() || self.common.options.skip_merged_crd) + && (mod_gen_ctx.skip.try_convert.is_present() + || self.common.options.skip_try_convert) + { + status_path + .as_ref() + .map(|status_path| quote! { , status = #status_path }) + } else { + let status_ident = &spec_gen_ctx.kubernetes_idents.status; + Some(quote! { , status = #status_ident }) + } } - (_, Some(status_ident)) => Some(quote! { , status = #status_ident }), - (_, _) => None, + (false, Some(status_path)) => Some(quote! { , status = #status_path }), + _ => None, }; let shortnames: TokenStream = spec_gen_ctx @@ -306,14 +316,7 @@ impl Struct { ) -> TokenStream { let enum_ident = &spec_gen_ctx.kubernetes_idents.kind; - // Only generate merged_crd associated function if not opted out - let merged_crd_fn = - if !mod_gen_ctx.skip.merged_crd.is_present() && !self.common.options.skip_merged_crd { - Some(self.generate_merged_crd_fn(mod_gen_ctx, spec_gen_ctx)) - } else { - None - }; - + let merged_crd_fn = self.generate_merged_crd_fn(mod_gen_ctx, spec_gen_ctx); let try_convert_fn = self.generate_try_convert_fn(versions, mod_gen_ctx, spec_gen_ctx); let from_json_value_fn = self.generate_from_json_value_fn(mod_gen_ctx, spec_gen_ctx); let into_json_value_fn = self.generate_into_json_value_fn(mod_gen_ctx, spec_gen_ctx); @@ -421,22 +424,84 @@ impl Struct { return None; } - if !mod_gen_ctx - .kubernetes_options - .experimental_conversion_tracking - .is_present() - { - return None; - } - let next_version = ver_ctx.next_version; let version = ver_ctx.version; next_version.map(|next_version| { - self.generate_tracking_from_impl(direction, version, next_version, mod_gen_ctx) + if mod_gen_ctx + .kubernetes_options + .experimental_conversion_tracking + .is_present() + { + self.generate_tracking_from_impl(direction, version, next_version, mod_gen_ctx) + } else { + self.generate_plain_from_impl(direction, version, next_version, mod_gen_ctx) + } }) } + fn generate_plain_from_impl( + &self, + direction: Direction, + version: &VersionDefinition, + next_version: &VersionDefinition, + mod_gen_ctx: ModuleGenerationContext<'_>, + ) -> TokenStream { + // TODO (@Techassi): A bunch this stuff is duplicated in self.generate_tracking_from_impl. + // Ideally we remove that duplication. + let from_struct_ident = &self.common.idents.parameter; + let struct_ident = &self.common.idents.original; + + // Include allow(deprecated) only when this or the next version is + // deprecated. Also include it, when a field in this or the next + // version is deprecated. + let allow_attribute = (version.deprecated.is_some() + || next_version.deprecated.is_some() + || self.is_any_field_deprecated(version) + || self.is_any_field_deprecated(next_version)) + .then(|| quote! { #[allow(deprecated)] }); + + // Only add the #[automatically_derived] attribute only if this impl is used + // outside of a module (in standalone mode). + let automatically_derived = mod_gen_ctx.automatically_derived_attr(); + + let fields = |direction: Direction| -> TokenStream { + self.fields + .iter() + .filter_map(|f| { + f.generate_for_from_impl(direction, version, next_version, from_struct_ident) + }) + .collect() + }; + + let (fields, for_module_ident, from_module_ident) = match direction { + direction @ Direction::Upgrade => { + let from_module_ident = &version.idents.module; + let for_module_ident = &next_version.idents.module; + + (fields(direction), for_module_ident, from_module_ident) + } + direction @ Direction::Downgrade => { + let from_module_ident = &next_version.idents.module; + let for_module_ident = &version.idents.module; + + (fields(direction), for_module_ident, from_module_ident) + } + }; + + quote! { + #automatically_derived + #allow_attribute + impl ::core::convert::From<#from_module_ident::#struct_ident> for #for_module_ident::#struct_ident { + fn from(#from_struct_ident: #from_module_ident::#struct_ident) -> Self { + Self { + #fields + } + } + } + } + } + /// Returns whether any field is deprecated in the provided `version`. fn is_any_field_deprecated(&self, version: &VersionDefinition) -> bool { // First, iterate over all fields. The `any` function will return true diff --git a/crates/stackable-versioned-macros/src/codegen/item/field.rs b/crates/stackable-versioned-macros/src/codegen/item/field.rs index 949a4c656..f88985c2e 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/field.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/field.rs @@ -331,16 +331,19 @@ impl VersionedField { Direction::Upgrade => { let next_change = changes.get_expect(&next_version.inner); - let ident = &self.ident; - let json_path_ident = format_ident!("__sv_{}_path", &self.ident.as_ident()); - match next_change { - ItemStatus::NotPresent | ItemStatus::NoChange { .. } => None, - _ => Some(quote! { - field_name if field_name == #json_path_ident => { - spec.#ident = serde_yaml::from_value(value).unwrap(); - }, - }), + // NOTE (@Techassi): We currently only support tracking added fields. As such + // we only need to generate code if the next change is "Addition". + ItemStatus::Addition { ident, .. } => { + let json_path_ident = format_ident!("__sv_{}_path", ident.as_ident()); + + Some(quote! { + field_name if field_name == #json_path_ident => { + spec.#ident = serde_yaml::from_value(value).unwrap(); + }, + }) + } + _ => None, } } Direction::Downgrade => None, @@ -369,14 +372,15 @@ impl VersionedField { let next_change = changes.get_expect(&next_version.inner); match next_change { - ItemStatus::NoChange { .. } | ItemStatus::NotPresent => None, - _ => { - let field_ident = format_ident!("__sv_{}_path", &self.ident.as_ident()); - let child_string = &self.ident.to_string(); + ItemStatus::Addition { ident, .. } => { + let field_ident = format_ident!("__sv_{}_path", ident.as_ident()); + let child_string = ident.to_string(); + Some(quote! { let #field_ident = ::stackable_versioned::jthong_path(parent, #child_string); }) } + _ => None, } } } From 26e5565d601adceae7328cdd2288541a6c946fb9 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 4 Jul 2025 09:54:34 +0200 Subject: [PATCH 08/16] test(stackable-versioned): Adjust test inputs and UI stderr snapshots --- .../tests/inputs/default/fail/deprecate.rs | 14 ---- .../inputs/default/fail/skip_from_all.rs | 24 ------ .../inputs/default/fail/skip_from_all.stderr | 35 --------- .../inputs/default/fail/skip_from_version.rs | 24 ------ .../default/fail/skip_from_version.stderr | 33 -------- .../default/fail/submodule_invalid_name.rs | 8 -- .../fail/submodule_invalid_name.stderr | 5 -- .../inputs/default/pass/attribute_enum.rs | 42 ---------- .../inputs/default/pass/attribute_struct.rs | 39 ---------- .../tests/inputs/default/pass/basic_struct.rs | 33 -------- .../inputs/default/pass/deprecate_enum.rs | 17 ----- .../inputs/default/pass/deprecate_struct.rs | 17 ----- .../inputs/default/pass/downgrade_with.rs | 24 ------ .../tests/inputs/default/pass/module.rs | 27 ------- .../inputs/default/pass/module_preserve.rs | 27 ------- .../tests/inputs/default/pass/rename.rs | 15 ---- .../default/pass/skip_from_for_version.rs | 18 ----- .../pass/skip_from_module_for_version.rs | 25 ------ .../tests/inputs/fail/applied_to_struct.rs | 6 ++ .../inputs/fail/applied_to_struct.stderr | 5 ++ .../inputs/{default => }/fail/changed.rs | 16 ++-- .../inputs/{default => }/fail/changed.stderr | 6 +- .../tests/inputs/fail/deprecate.rs | 16 ++++ .../{default => }/fail/deprecate.stderr | 2 +- .../tests/inputs/fail/spec_suffix.rs | 23 ++++++ .../tests/inputs/fail/spec_suffix.stderr | 5 ++ .../inputs/fail/submodule_invalid_name.rs | 8 ++ .../inputs/fail/submodule_invalid_name.stderr | 5 ++ .../fail/submodule_use_statement.rs | 0 .../fail/submodule_use_statement.stderr | 2 +- .../tests/inputs/fail/unknown_version.rs | 11 +++ .../tests/inputs/fail/unknown_version.stderr | 5 ++ .../pass => ignored}/enum_data_simple.rs | 0 .../pass => ignored}/generics_defaults.rs | 0 .../pass => ignored}/generics_enum.rs | 0 .../pass => ignored}/generics_module.rs | 0 .../pass => ignored}/generics_struct.rs | 0 .../tests/inputs/k8s/fail/crd.rs | 25 ------ .../tests/inputs/k8s/fail/crd.stderr | 11 --- .../tests/inputs/k8s/pass/basic.rs | 37 --------- .../tests/inputs/k8s/pass/crate_overrides.rs | 25 ------ .../tests/inputs/k8s/pass/renamed_kind.rs | 19 ----- .../tests/inputs/k8s/pass/shortnames.rs | 13 ---- .../tests/inputs/{default => }/pass/added.rs | 14 ++-- .../tests/inputs/pass/basic.rs | 44 +++++++++++ .../{k8s => }/pass/conversion_tracking.rs | 24 +++--- .../tests/inputs/pass/crate_overrides.rs | 30 ++++++++ .../tests/inputs/pass/docs.rs | 41 ++++++++++ .../tests/inputs/pass/downgrade_with.rs | 26 +++++++ .../tests/inputs/{k8s => }/pass/module.rs | 4 +- .../inputs/{k8s => }/pass/module_preserve.rs | 4 +- .../renamed_field.rs} | 9 +-- .../tests/inputs/pass/renamed_kind.rs | 26 +++++++ .../tests/inputs/pass/shortnames.rs | 18 +++++ .../inputs/{default => }/pass/submodule.rs | 0 .../tests/trybuild.rs | 76 ++++++------------- 56 files changed, 335 insertions(+), 648 deletions(-) delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/fail/deprecate.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_all.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_all.stderr delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_version.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_version.stderr delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/fail/submodule_invalid_name.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/fail/submodule_invalid_name.stderr delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/pass/attribute_enum.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/pass/attribute_struct.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/pass/basic_struct.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/pass/deprecate_enum.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/pass/deprecate_struct.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/pass/downgrade_with.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/pass/module.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/pass/module_preserve.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/pass/rename.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/pass/skip_from_for_version.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/default/pass/skip_from_module_for_version.rs create mode 100644 crates/stackable-versioned-macros/tests/inputs/fail/applied_to_struct.rs create mode 100644 crates/stackable-versioned-macros/tests/inputs/fail/applied_to_struct.stderr rename crates/stackable-versioned-macros/tests/inputs/{default => }/fail/changed.rs (63%) rename crates/stackable-versioned-macros/tests/inputs/{default => }/fail/changed.stderr (80%) create mode 100644 crates/stackable-versioned-macros/tests/inputs/fail/deprecate.rs rename crates/stackable-versioned-macros/tests/inputs/{default => }/fail/deprecate.stderr (72%) create mode 100644 crates/stackable-versioned-macros/tests/inputs/fail/spec_suffix.rs create mode 100644 crates/stackable-versioned-macros/tests/inputs/fail/spec_suffix.stderr create mode 100644 crates/stackable-versioned-macros/tests/inputs/fail/submodule_invalid_name.rs create mode 100644 crates/stackable-versioned-macros/tests/inputs/fail/submodule_invalid_name.stderr rename crates/stackable-versioned-macros/tests/inputs/{default => }/fail/submodule_use_statement.rs (100%) rename crates/stackable-versioned-macros/tests/inputs/{default => }/fail/submodule_use_statement.stderr (62%) create mode 100644 crates/stackable-versioned-macros/tests/inputs/fail/unknown_version.rs create mode 100644 crates/stackable-versioned-macros/tests/inputs/fail/unknown_version.stderr rename crates/stackable-versioned-macros/tests/inputs/{default/pass => ignored}/enum_data_simple.rs (100%) rename crates/stackable-versioned-macros/tests/inputs/{default/pass => ignored}/generics_defaults.rs (100%) rename crates/stackable-versioned-macros/tests/inputs/{default/pass => ignored}/generics_enum.rs (100%) rename crates/stackable-versioned-macros/tests/inputs/{default/pass => ignored}/generics_module.rs (100%) rename crates/stackable-versioned-macros/tests/inputs/{default/pass => ignored}/generics_struct.rs (100%) delete mode 100644 crates/stackable-versioned-macros/tests/inputs/k8s/fail/crd.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/k8s/fail/crd.stderr delete mode 100644 crates/stackable-versioned-macros/tests/inputs/k8s/pass/basic.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/k8s/pass/crate_overrides.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/k8s/pass/renamed_kind.rs delete mode 100644 crates/stackable-versioned-macros/tests/inputs/k8s/pass/shortnames.rs rename crates/stackable-versioned-macros/tests/inputs/{default => }/pass/added.rs (52%) create mode 100644 crates/stackable-versioned-macros/tests/inputs/pass/basic.rs rename crates/stackable-versioned-macros/tests/inputs/{k8s => }/pass/conversion_tracking.rs (51%) create mode 100644 crates/stackable-versioned-macros/tests/inputs/pass/crate_overrides.rs create mode 100644 crates/stackable-versioned-macros/tests/inputs/pass/docs.rs create mode 100644 crates/stackable-versioned-macros/tests/inputs/pass/downgrade_with.rs rename crates/stackable-versioned-macros/tests/inputs/{k8s => }/pass/module.rs (89%) rename crates/stackable-versioned-macros/tests/inputs/{k8s => }/pass/module_preserve.rs (89%) rename crates/stackable-versioned-macros/tests/inputs/{default/pass/skip_from_module.rs => pass/renamed_field.rs} (60%) create mode 100644 crates/stackable-versioned-macros/tests/inputs/pass/renamed_kind.rs create mode 100644 crates/stackable-versioned-macros/tests/inputs/pass/shortnames.rs rename crates/stackable-versioned-macros/tests/inputs/{default => }/pass/submodule.rs (100%) diff --git a/crates/stackable-versioned-macros/tests/inputs/default/fail/deprecate.rs b/crates/stackable-versioned-macros/tests/inputs/default/fail/deprecate.rs deleted file mode 100644 index 65ba4c2c6..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/fail/deprecate.rs +++ /dev/null @@ -1,14 +0,0 @@ -use stackable_versioned_macros::versioned; - -fn main() { - #[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1"), - version(name = "v1") - )] - struct Foo { - #[deprecated] - bar: usize, - baz: bool, - } -} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_all.rs b/crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_all.rs deleted file mode 100644 index b883a501b..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_all.rs +++ /dev/null @@ -1,24 +0,0 @@ -use stackable_versioned_macros::versioned; - -fn main() { - #[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1"), - version(name = "v1"), - options(skip(from)) - )] - pub struct Foo { - #[versioned( - added(since = "v1beta1"), - deprecated(since = "v1", note = "not needed") - )] - deprecated_bar: usize, - baz: bool, - } - - let foo_v1alpha1 = v1alpha1::Foo { baz: true }; - - // There are no From impls for any version. You need to convert it manually. - #[allow(dead_code)] - let foo_v1beta1 = v1beta1::Foo::from(foo_v1alpha1); -} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_all.stderr b/crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_all.stderr deleted file mode 100644 index d7733a74f..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_all.stderr +++ /dev/null @@ -1,35 +0,0 @@ -error[E0308]: mismatched types - --> tests/inputs/default/fail/skip_from_all.rs:23:42 - | -23 | let foo_v1beta1 = v1beta1::Foo::from(foo_v1alpha1); - | ------------------ ^^^^^^^^^^^^ expected `v1beta1::Foo`, found `v1alpha1::Foo` - | | - | arguments to this function are incorrect - | - = 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/inputs/default/fail/skip_from_all.rs:4:5 - | -4 | / #[versioned( -5 | | version(name = "v1alpha1"), -6 | | version(name = "v1beta1"), -7 | | version(name = "v1"), -8 | | options(skip(from)) -9 | | )] - | |______^ -note: `v1beta1::Foo` is defined in module `crate::main::v1beta1` of the current crate - --> tests/inputs/default/fail/skip_from_all.rs:4:5 - | -4 | / #[versioned( -5 | | version(name = "v1alpha1"), -6 | | version(name = "v1beta1"), -7 | | version(name = "v1"), -8 | | options(skip(from)) -9 | | )] - | |______^ -note: associated function defined here - --> $RUST/core/src/convert/mod.rs - | - | fn from(value: T) -> Self; - | ^^^^ - = note: this error originates in the attribute macro `versioned` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_version.rs b/crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_version.rs deleted file mode 100644 index e08fc2325..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_version.rs +++ /dev/null @@ -1,24 +0,0 @@ -use stackable_versioned_macros::versioned; - -fn main() { - #[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1", skip(from)), - version(name = "v1") - )] - pub struct Foo { - #[versioned( - added(since = "v1beta1"), - deprecated(since = "v1", note = "not needed") - )] - deprecated_bar: usize, - baz: bool, - } - - let foo_v1alpha1 = v1alpha1::Foo { baz: true }; - let foo_v1beta1 = v1beta1::Foo::from(foo_v1alpha1); - - #[allow(dead_code)] - // v1beta1 has no From impl. You need to convert it manually. - let foo_v1 = v1::Foo::from(foo_v1beta1); -} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_version.stderr b/crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_version.stderr deleted file mode 100644 index 3b408c112..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/fail/skip_from_version.stderr +++ /dev/null @@ -1,33 +0,0 @@ -error[E0308]: mismatched types - --> tests/inputs/default/fail/skip_from_version.rs:23:32 - | -23 | let foo_v1 = v1::Foo::from(foo_v1beta1); - | ------------- ^^^^^^^^^^^ expected `main::v1::Foo`, found `v1beta1::Foo` - | | - | arguments to this function are incorrect - | - = 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/inputs/default/fail/skip_from_version.rs:4:5 - | -4 | / #[versioned( -5 | | version(name = "v1alpha1"), -6 | | version(name = "v1beta1", skip(from)), -7 | | version(name = "v1") -8 | | )] - | |______^ -note: `main::v1::Foo` is defined in module `crate::main::v1` of the current crate - --> tests/inputs/default/fail/skip_from_version.rs:4:5 - | -4 | / #[versioned( -5 | | version(name = "v1alpha1"), -6 | | version(name = "v1beta1", skip(from)), -7 | | version(name = "v1") -8 | | )] - | |______^ -note: associated function defined here - --> $RUST/core/src/convert/mod.rs - | - | fn from(value: T) -> Self; - | ^^^^ - = note: this error originates in the attribute macro `versioned` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/stackable-versioned-macros/tests/inputs/default/fail/submodule_invalid_name.rs b/crates/stackable-versioned-macros/tests/inputs/default/fail/submodule_invalid_name.rs deleted file mode 100644 index 2ce7a5997..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/fail/submodule_invalid_name.rs +++ /dev/null @@ -1,8 +0,0 @@ -use stackable_versioned_macros::versioned; - -fn main() { - #[versioned(version(name = "v1alpha1"))] - mod versioned { - mod v1alpha2 {} - } -} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/fail/submodule_invalid_name.stderr b/crates/stackable-versioned-macros/tests/inputs/default/fail/submodule_invalid_name.stderr deleted file mode 100644 index 80e82ef9f..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/fail/submodule_invalid_name.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: submodules must use names which are defined as `version`s - --> tests/inputs/default/fail/submodule_invalid_name.rs:6:9 - | -6 | mod v1alpha2 {} - | ^^^ diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/attribute_enum.rs b/crates/stackable-versioned-macros/tests/inputs/default/pass/attribute_enum.rs deleted file mode 100644 index 305774813..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/pass/attribute_enum.rs +++ /dev/null @@ -1,42 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned( - version(name = "v1alpha1"), - version( - name = "v1beta1", - doc = r#" - Additional docs for this version which are purposefully long to - show how manual line wrapping works. \ - Multi-line docs are also supported, as per regular doc-comments. - "# - ), - version(name = "v1beta2"), - version(name = "v1"), - version(name = "v2"), - options(skip(from)) -)] -// --- -#[derive(Default)] -enum Foo { - /// This variant is available in every version (so far). - #[default] - Foo, - - /// Keep the main field docs the same, even after the field is - /// deprecated. - #[versioned(deprecated(since = "v1beta1", note = "gone"))] - DeprecatedBar, - - /// This is for baz - #[versioned(added(since = "v1beta1"))] - // Just to check stackable-versioned deprecation warning appears. - // #[deprecated] - Baz, - - /// This is will keep changing over time. - #[versioned(changed(since = "v1beta1", from_name = "Qoox"))] - #[versioned(changed(since = "v1", from_name = "Qaax"))] - Quux, -} -// --- -fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/attribute_struct.rs b/crates/stackable-versioned-macros/tests/inputs/default/pass/attribute_struct.rs deleted file mode 100644 index 0749a040a..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/pass/attribute_struct.rs +++ /dev/null @@ -1,39 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned( - version(name = "v1alpha1"), - version( - name = "v1beta1", - doc = r#" - Additional docs for this version which are purposefully long to - show how manual line wrapping works. \ - Multi-line docs are also supported, as per regular doc-comments. - "# - ), - version(name = "v1beta2"), - version(name = "v1"), - version(name = "v2"), - options(skip(from)) -)] -// --- -/// Test -#[derive(Default)] -struct Foo { - /// This field is available in every version (so far). - foo: String, - - /// Keep the main field docs the same, even after the field is deprecated. - #[versioned(deprecated(since = "v1beta1", note = "gone"))] - deprecated_bar: String, - - /// This is for baz - #[versioned(added(since = "v1beta1"))] - baz: String, - - /// This is will keep changing over time. - #[versioned(changed(since = "v1beta1", from_name = "qoox"))] - #[versioned(changed(since = "v1", from_name = "qaax"))] - quux: String, -} -// --- -fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/basic_struct.rs b/crates/stackable-versioned-macros/tests/inputs/default/pass/basic_struct.rs deleted file mode 100644 index 488ea73ad..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/pass/basic_struct.rs +++ /dev/null @@ -1,33 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned( - version(name = "v1alpha1", deprecated), - version(name = "v1beta1"), - version(name = "v1"), - version(name = "v2"), - version(name = "v3") -)] -// --- -pub(crate) struct Foo { - #[versioned(added(since = "v1"))] - foo: String, - - #[versioned( - changed(since = "v1beta1", from_name = "jjj", from_type = "u8", downgrade_with = u16_to_u8), - changed(since = "v1", from_type = "u16", downgrade_with = usize_to_u16), - deprecated(since = "v2", note = "not empty") - )] - /// Test - deprecated_bar: usize, - baz: bool, -} -// --- -fn main() {} - -fn usize_to_u16(input: usize) -> u16 { - input.try_into().unwrap() -} - -fn u16_to_u8(input: u16) -> u8 { - input.try_into().unwrap() -} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/deprecate_enum.rs b/crates/stackable-versioned-macros/tests/inputs/default/pass/deprecate_enum.rs deleted file mode 100644 index d996a9424..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/pass/deprecate_enum.rs +++ /dev/null @@ -1,17 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1"), - version(name = "v1"), - version(name = "v2"), - version(name = "v3") -)] -// --- -enum Foo { - #[versioned(deprecated(since = "v1"))] - DeprecatedBar, - Baz, -} -// --- -fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/deprecate_struct.rs b/crates/stackable-versioned-macros/tests/inputs/default/pass/deprecate_struct.rs deleted file mode 100644 index 666a1dbc5..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/pass/deprecate_struct.rs +++ /dev/null @@ -1,17 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1"), - version(name = "v1"), - version(name = "v2"), - version(name = "v3") -)] -// --- -struct Foo { - #[versioned(deprecated(since = "v1", note = "gone"))] - deprecated_bar: usize, - baz: bool, -} -// --- -fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/downgrade_with.rs b/crates/stackable-versioned-macros/tests/inputs/default/pass/downgrade_with.rs deleted file mode 100644 index e589b2d57..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/pass/downgrade_with.rs +++ /dev/null @@ -1,24 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned(version(name = "v1alpha1"), version(name = "v1"), version(name = "v2"))] -// --- -struct Foo { - #[versioned( - // This tests two additional things: - // - that both unquoted and quoted usage works - // - that the renamed name does get picked up correctly by the conversion function - changed(since = "v1", from_type = "u16", from_name = "bar", downgrade_with = u32_to_u16), - changed(since = "v2", from_type = "u32", downgrade_with = "u64_to_u32") - )] - baz: u64, -} -// --- -fn main() {} - -fn u32_to_u16(input: u32) -> u16 { - input.try_into().unwrap() -} - -fn u64_to_u32(input: u64) -> u32 { - input.try_into().unwrap() -} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/module.rs b/crates/stackable-versioned-macros/tests/inputs/default/pass/module.rs deleted file mode 100644 index e87d10a2f..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/pass/module.rs +++ /dev/null @@ -1,27 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned( - version(name = "v1alpha1"), - version(name = "v1"), - version(name = "v2alpha1") -)] -// --- -pub(crate) mod versioned { - pub struct Foo { - bar: usize, - - #[versioned(added(since = "v1"))] - baz: bool, - - #[versioned(deprecated(since = "v2alpha1"))] - deprecated_foo: String, - } - - // The following attribute is just to ensure no strange behavior occurs. - #[versioned] - pub struct Bar { - baz: String, - } -} -// --- -fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/module_preserve.rs b/crates/stackable-versioned-macros/tests/inputs/default/pass/module_preserve.rs deleted file mode 100644 index 8bbb08224..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/pass/module_preserve.rs +++ /dev/null @@ -1,27 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned( - version(name = "v1alpha1"), - version(name = "v1"), - version(name = "v2alpha1"), - options(preserve_module) -)] -// --- -pub(crate) mod versioned { - pub struct Foo { - bar: usize, - - #[versioned(added(since = "v1"))] - baz: bool, - - #[versioned(deprecated(since = "v2alpha1"))] - deprecated_foo: String, - } - - #[versioned] - pub struct Bar { - baz: String, - } -} -// --- -fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/rename.rs b/crates/stackable-versioned-macros/tests/inputs/default/pass/rename.rs deleted file mode 100644 index bad659ea9..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/pass/rename.rs +++ /dev/null @@ -1,15 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1"), - version(name = "v1") -)] -// --- -struct Foo { - #[versioned(changed(since = "v1beta1", from_name = "bat"))] - bar: usize, - baz: bool, -} -// --- -fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/skip_from_for_version.rs b/crates/stackable-versioned-macros/tests/inputs/default/pass/skip_from_for_version.rs deleted file mode 100644 index f80c496d9..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/pass/skip_from_for_version.rs +++ /dev/null @@ -1,18 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1", skip(from)), - version(name = "v1") -)] -// --- -pub struct Foo { - #[versioned( - added(since = "v1beta1"), - deprecated(since = "v1", note = "not needed") - )] - deprecated_bar: usize, - baz: bool, -} -// --- -fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/skip_from_module_for_version.rs b/crates/stackable-versioned-macros/tests/inputs/default/pass/skip_from_module_for_version.rs deleted file mode 100644 index 456cb4ffb..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/default/pass/skip_from_module_for_version.rs +++ /dev/null @@ -1,25 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1", skip(from)), - version(name = "v1") -)] -// --- -pub(crate) mod versioned { - pub struct Foo { - bar: usize, - - #[versioned(added(since = "v1beta1"))] - baz: bool, - } - - pub struct Bar { - foo: usize, - - #[versioned(added(since = "v1beta1"))] - faz: bool, - } -} -// --- -fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/fail/applied_to_struct.rs b/crates/stackable-versioned-macros/tests/inputs/fail/applied_to_struct.rs new file mode 100644 index 000000000..a69000a94 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/fail/applied_to_struct.rs @@ -0,0 +1,6 @@ +use stackable_versioned_macros::versioned; + +#[versioned()] +pub struct Foo {} + +fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/fail/applied_to_struct.stderr b/crates/stackable-versioned-macros/tests/inputs/fail/applied_to_struct.stderr new file mode 100644 index 000000000..848a80516 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/fail/applied_to_struct.stderr @@ -0,0 +1,5 @@ +error: attribute macro `versioned` can be only be applied to modules + --> tests/inputs/fail/applied_to_struct.rs:4:1 + | +4 | pub struct Foo {} + | ^^^ diff --git a/crates/stackable-versioned-macros/tests/inputs/default/fail/changed.rs b/crates/stackable-versioned-macros/tests/inputs/fail/changed.rs similarity index 63% rename from crates/stackable-versioned-macros/tests/inputs/default/fail/changed.rs rename to crates/stackable-versioned-macros/tests/inputs/fail/changed.rs index 1ca047717..dbcaefc96 100644 --- a/crates/stackable-versioned-macros/tests/inputs/default/fail/changed.rs +++ b/crates/stackable-versioned-macros/tests/inputs/fail/changed.rs @@ -1,12 +1,12 @@ use stackable_versioned_macros::versioned; -fn main() { - #[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1"), - version(name = "v1"), - version(name = "v2") - )] +#[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + version(name = "v1"), + version(name = "v2") +)] +mod versioned { struct Foo { #[versioned( changed(since = "v1beta1", from_name = "deprecated_bar"), @@ -16,3 +16,5 @@ fn main() { bar: usize, } } + +fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/fail/changed.stderr b/crates/stackable-versioned-macros/tests/inputs/fail/changed.stderr similarity index 80% rename from crates/stackable-versioned-macros/tests/inputs/default/fail/changed.stderr rename to crates/stackable-versioned-macros/tests/inputs/fail/changed.stderr index da1a7063b..61d10138f 100644 --- a/crates/stackable-versioned-macros/tests/inputs/default/fail/changed.stderr +++ b/crates/stackable-versioned-macros/tests/inputs/fail/changed.stderr @@ -1,17 +1,17 @@ error: the previous name must not start with the deprecation prefix - --> tests/inputs/default/fail/changed.rs:12:52 + --> tests/inputs/fail/changed.rs:12:52 | 12 | changed(since = "v1beta1", from_name = "deprecated_bar"), | ^^^^^^^^^^^^^^^^ error: the previous name must not start with the deprecation prefix - --> tests/inputs/default/fail/changed.rs:13:47 + --> tests/inputs/fail/changed.rs:13:47 | 13 | changed(since = "v1", from_name = "deprecated_baz"), | ^^^^^^^^^^^^^^^^ error: both `from_name` and `from_type` are unset. Is this `changed()` action needed? - --> tests/inputs/default/fail/changed.rs:14:29 + --> tests/inputs/fail/changed.rs:14:29 | 14 | changed(since = "v2") | ^^^^ diff --git a/crates/stackable-versioned-macros/tests/inputs/fail/deprecate.rs b/crates/stackable-versioned-macros/tests/inputs/fail/deprecate.rs new file mode 100644 index 000000000..682eb0be1 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/fail/deprecate.rs @@ -0,0 +1,16 @@ +use stackable_versioned_macros::versioned; + +#[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + version(name = "v1") +)] +mod versioned { + struct Foo { + #[deprecated] + bar: usize, + baz: bool, + } +} + +fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/fail/deprecate.stderr b/crates/stackable-versioned-macros/tests/inputs/fail/deprecate.stderr similarity index 72% rename from crates/stackable-versioned-macros/tests/inputs/default/fail/deprecate.stderr rename to crates/stackable-versioned-macros/tests/inputs/fail/deprecate.stderr index 46e1bbdcd..8e32e5dad 100644 --- a/crates/stackable-versioned-macros/tests/inputs/default/fail/deprecate.stderr +++ b/crates/stackable-versioned-macros/tests/inputs/fail/deprecate.stderr @@ -1,5 +1,5 @@ error: deprecation must be done using `#[versioned(deprecated(since = "VERSION"))]` - --> tests/inputs/default/fail/deprecate.rs:10:9 + --> tests/inputs/fail/deprecate.rs:10:9 | 10 | #[deprecated] | ^ diff --git a/crates/stackable-versioned-macros/tests/inputs/fail/spec_suffix.rs b/crates/stackable-versioned-macros/tests/inputs/fail/spec_suffix.rs new file mode 100644 index 000000000..68c3f323e --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/fail/spec_suffix.rs @@ -0,0 +1,23 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use stackable_versioned_macros::versioned; + +#[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + version(name = "v1") +)] +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +mod versioned { + #[versioned(crd(group = "stackable.tech"))] + pub struct Foo { + #[versioned( + added(since = "v1beta1"), + changed(since = "v1", from_name = "bah", from_type = "u16") + )] + bar: usize, + baz: bool, + } +} + +fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/fail/spec_suffix.stderr b/crates/stackable-versioned-macros/tests/inputs/fail/spec_suffix.stderr new file mode 100644 index 000000000..51d5e55c5 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/fail/spec_suffix.stderr @@ -0,0 +1,5 @@ +error: struct name needs to include the `Spec` suffix if CRD features are enabled via `#[versioned(crd())]` + --> tests/inputs/fail/spec_suffix.rs:13:16 + | +13 | pub struct Foo { + | ^^^ diff --git a/crates/stackable-versioned-macros/tests/inputs/fail/submodule_invalid_name.rs b/crates/stackable-versioned-macros/tests/inputs/fail/submodule_invalid_name.rs new file mode 100644 index 000000000..0437d77bd --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/fail/submodule_invalid_name.rs @@ -0,0 +1,8 @@ +use stackable_versioned_macros::versioned; + +#[versioned(version(name = "v1alpha1"))] +mod versioned { + mod v1alpha2 {} +} + +fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/fail/submodule_invalid_name.stderr b/crates/stackable-versioned-macros/tests/inputs/fail/submodule_invalid_name.stderr new file mode 100644 index 000000000..cdd5576c4 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/fail/submodule_invalid_name.stderr @@ -0,0 +1,5 @@ +error: submodules must use names which are defined as `version`s + --> tests/inputs/fail/submodule_invalid_name.rs:5:5 + | +5 | mod v1alpha2 {} + | ^^^ diff --git a/crates/stackable-versioned-macros/tests/inputs/default/fail/submodule_use_statement.rs b/crates/stackable-versioned-macros/tests/inputs/fail/submodule_use_statement.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/inputs/default/fail/submodule_use_statement.rs rename to crates/stackable-versioned-macros/tests/inputs/fail/submodule_use_statement.rs diff --git a/crates/stackable-versioned-macros/tests/inputs/default/fail/submodule_use_statement.stderr b/crates/stackable-versioned-macros/tests/inputs/fail/submodule_use_statement.stderr similarity index 62% rename from crates/stackable-versioned-macros/tests/inputs/default/fail/submodule_use_statement.stderr rename to crates/stackable-versioned-macros/tests/inputs/fail/submodule_use_statement.stderr index 20c7f4e5a..9d6ee3893 100644 --- a/crates/stackable-versioned-macros/tests/inputs/default/fail/submodule_use_statement.stderr +++ b/crates/stackable-versioned-macros/tests/inputs/fail/submodule_use_statement.stderr @@ -1,5 +1,5 @@ error: submodules must only define `use` statements - --> tests/inputs/default/fail/submodule_use_statement.rs:7:13 + --> tests/inputs/fail/submodule_use_statement.rs:7:13 | 7 | struct Foo; | ^^^^^^ diff --git a/crates/stackable-versioned-macros/tests/inputs/fail/unknown_version.rs b/crates/stackable-versioned-macros/tests/inputs/fail/unknown_version.rs new file mode 100644 index 000000000..5847817c6 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/fail/unknown_version.rs @@ -0,0 +1,11 @@ +use stackable_versioned_macros::versioned; + +#[versioned(version(name = "v1alpha1"))] +mod versioned { + struct Foo { + #[versioned(added(since = "v1alpha2"))] + bar: usize, + } +} + +fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/fail/unknown_version.stderr b/crates/stackable-versioned-macros/tests/inputs/fail/unknown_version.stderr new file mode 100644 index 000000000..070bd3e2a --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/fail/unknown_version.stderr @@ -0,0 +1,5 @@ +error: the `added` action uses a version which is not declared via `#[versioned(version)]` + --> tests/inputs/fail/unknown_version.rs:6:35 + | +6 | #[versioned(added(since = "v1alpha2"))] + | ^^^^^^^^^^ diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/enum_data_simple.rs b/crates/stackable-versioned-macros/tests/inputs/ignored/enum_data_simple.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/inputs/default/pass/enum_data_simple.rs rename to crates/stackable-versioned-macros/tests/inputs/ignored/enum_data_simple.rs diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/generics_defaults.rs b/crates/stackable-versioned-macros/tests/inputs/ignored/generics_defaults.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/inputs/default/pass/generics_defaults.rs rename to crates/stackable-versioned-macros/tests/inputs/ignored/generics_defaults.rs diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/generics_enum.rs b/crates/stackable-versioned-macros/tests/inputs/ignored/generics_enum.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/inputs/default/pass/generics_enum.rs rename to crates/stackable-versioned-macros/tests/inputs/ignored/generics_enum.rs diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/generics_module.rs b/crates/stackable-versioned-macros/tests/inputs/ignored/generics_module.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/inputs/default/pass/generics_module.rs rename to crates/stackable-versioned-macros/tests/inputs/ignored/generics_module.rs diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/generics_struct.rs b/crates/stackable-versioned-macros/tests/inputs/ignored/generics_struct.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/inputs/default/pass/generics_struct.rs rename to crates/stackable-versioned-macros/tests/inputs/ignored/generics_struct.rs diff --git a/crates/stackable-versioned-macros/tests/inputs/k8s/fail/crd.rs b/crates/stackable-versioned-macros/tests/inputs/k8s/fail/crd.rs deleted file mode 100644 index e88cbd7e6..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/k8s/fail/crd.rs +++ /dev/null @@ -1,25 +0,0 @@ -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/inputs/k8s/fail/crd.stderr b/crates/stackable-versioned-macros/tests/inputs/k8s/fail/crd.stderr deleted file mode 100644 index 2fdc1a566..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/k8s/fail/crd.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: struct name needs to include the `Spec` suffix if Kubernetes features are enabled via `#[versioned(k8s())]` - --> tests/inputs/k8s/fail/crd.rs:14:16 - | -14 | pub struct Foo { - | ^^^ - -error[E0433]: failed to resolve: use of undeclared type `Foo` - --> tests/inputs/k8s/fail/crd.rs:23:22 - | -23 | let merged_crd = Foo::merged_crd("v1").unwrap(); - | ^^^ use of undeclared type `Foo` diff --git a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/basic.rs b/crates/stackable-versioned-macros/tests/inputs/k8s/pass/basic.rs deleted file mode 100644 index a64ca4c96..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/basic.rs +++ /dev/null @@ -1,37 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1"), - version(name = "v1"), - k8s( - group = "stackable.tech", - singular = "foo", - plural = "foos", - status = FooStatus, - namespaced, - ) -)] -// --- -#[derive( - Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema, kube::CustomResource, -)] -pub(crate) struct FooSpec { - #[versioned( - added(since = "v1beta1"), - changed(since = "v1", from_name = "bah", from_type = "u16", downgrade_with = usize_to_u16) - )] - bar: usize, - baz: bool, -} -// --- -fn main() {} - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] -pub struct FooStatus { - is_foo: bool, -} - -fn usize_to_u16(input: usize) -> u16 { - input.try_into().unwrap() -} diff --git a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/crate_overrides.rs b/crates/stackable-versioned-macros/tests/inputs/k8s/pass/crate_overrides.rs deleted file mode 100644 index ff3dacdb9..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/crate_overrides.rs +++ /dev/null @@ -1,25 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1"), - version(name = "v1"), - k8s( - group = "foo.example.org", - crates( - kube_core = ::kube::core, - schemars = ::schemars - ) - ) -)] -// --- -#[derive( - Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema, kube::CustomResource, -)] -pub struct FooSpec { - #[versioned(added(since = "v1beta1"), changed(since = "v1", from_name = "bah"))] - bar: usize, - baz: bool, -} -// --- -fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/renamed_kind.rs b/crates/stackable-versioned-macros/tests/inputs/k8s/pass/renamed_kind.rs deleted file mode 100644 index 53f479b9a..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/renamed_kind.rs +++ /dev/null @@ -1,19 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1"), - version(name = "v1"), - k8s(group = "stackable.tech", kind = "FooBar", namespaced) -)] -// --- -#[derive( - Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema, kube::CustomResource, -)] -pub struct FooSpec { - #[versioned(added(since = "v1beta1"), changed(since = "v1", from_name = "bah"))] - bar: usize, - baz: bool, -} -// --- -fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/shortnames.rs b/crates/stackable-versioned-macros/tests/inputs/k8s/pass/shortnames.rs deleted file mode 100644 index 9ae034db3..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/shortnames.rs +++ /dev/null @@ -1,13 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned( - version(name = "v1alpha1"), - k8s(group = "stackable.tech", shortname = "f", shortname = "fo",) -)] -// --- -#[derive( - Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema, kube::CustomResource, -)] -pub(crate) struct FooSpec {} -// --- -fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/added.rs b/crates/stackable-versioned-macros/tests/inputs/pass/added.rs similarity index 52% rename from crates/stackable-versioned-macros/tests/inputs/default/pass/added.rs rename to crates/stackable-versioned-macros/tests/inputs/pass/added.rs index 57ae9d420..33b2af588 100644 --- a/crates/stackable-versioned-macros/tests/inputs/default/pass/added.rs +++ b/crates/stackable-versioned-macros/tests/inputs/pass/added.rs @@ -7,14 +7,16 @@ use stackable_versioned_macros::versioned; version(name = "v1") )] // --- -struct Foo { - username: String, +mod versioned { + struct Foo { + username: String, - #[versioned(added(since = "v1alpha2", default = default_first_name))] - first_name: String, + #[versioned(added(since = "v1alpha2", default = default_first_name))] + first_name: String, - #[versioned(added(since = "v1beta1"))] - last_name: String, + #[versioned(added(since = "v1beta1"))] + last_name: String, + } } // --- fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/pass/basic.rs b/crates/stackable-versioned-macros/tests/inputs/pass/basic.rs new file mode 100644 index 000000000..5cf71b155 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/pass/basic.rs @@ -0,0 +1,44 @@ +use stackable_versioned::versioned; +// --- +#[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + version(name = "v1") +)] +// --- +pub(crate) mod versioned { + #[versioned(crd( + group = "stackable.tech", + singular = "foo", + plural = "foos", + status = FooStatus, + namespaced, + ))] + #[derive( + Clone, + Debug, + serde::Deserialize, + serde::Serialize, + schemars::JsonSchema, + kube::CustomResource, + )] + struct FooSpec { + #[versioned( + added(since = "v1beta1"), + changed(since = "v1", from_name = "bah", from_type = "u16", downgrade_with = usize_to_u16) + )] + bar: usize, + baz: bool, + } +} +// --- +fn main() {} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +pub struct FooStatus { + is_foo: bool, +} + +fn usize_to_u16(input: usize) -> u16 { + input.try_into().unwrap_or(u16::MAX) +} diff --git a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/conversion_tracking.rs b/crates/stackable-versioned-macros/tests/inputs/pass/conversion_tracking.rs similarity index 51% rename from crates/stackable-versioned-macros/tests/inputs/k8s/pass/conversion_tracking.rs rename to crates/stackable-versioned-macros/tests/inputs/pass/conversion_tracking.rs index 325256f4b..8a31a81ff 100644 --- a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/conversion_tracking.rs +++ b/crates/stackable-versioned-macros/tests/inputs/pass/conversion_tracking.rs @@ -7,23 +7,25 @@ use stackable_versioned::versioned; version(name = "v1alpha1"), version(name = "v1beta1"), version(name = "v1"), - k8s( - group = "stackable.tech", - status = FooStatus, - options(experimental_conversion_tracking), - ) + options(k8s(experimental_conversion_tracking)) )] // --- -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] -pub(crate) struct FooSpec { - #[versioned(added(since = "v1beta1"), changed(since = "v1", from_name = "bah"))] - bar: usize, - baz: bool, +pub(crate) mod versioned { + #[versioned(crd( + group = "stackable.tech", + status = FooStatus, + ))] + #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] + pub(crate) struct FooSpec { + #[versioned(added(since = "v1beta1"), changed(since = "v1", from_name = "bah"))] + bar: usize, + baz: bool, + } } // --- fn main() {} -#[derive(Clone, Debug, JsonSchema, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, JsonSchema, Deserialize, Serialize)] pub struct FooStatus { foo: String, } diff --git a/crates/stackable-versioned-macros/tests/inputs/pass/crate_overrides.rs b/crates/stackable-versioned-macros/tests/inputs/pass/crate_overrides.rs new file mode 100644 index 000000000..6c453f44e --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/pass/crate_overrides.rs @@ -0,0 +1,30 @@ +use stackable_versioned::versioned; +// --- +#[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + version(name = "v1"), + crates( + kube_core = ::kube::core, + schemars = ::schemars + ) +)] +// --- +pub mod versioned { + #[versioned(crd(group = "foo.example.org"))] + #[derive( + Clone, + Debug, + serde::Deserialize, + serde::Serialize, + schemars::JsonSchema, + kube::CustomResource, + )] + pub struct FooSpec { + #[versioned(added(since = "v1beta1"), changed(since = "v1", from_name = "bah"))] + bar: usize, + baz: bool, + } +} +// --- +fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/pass/docs.rs b/crates/stackable-versioned-macros/tests/inputs/pass/docs.rs new file mode 100644 index 000000000..23888ab90 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/pass/docs.rs @@ -0,0 +1,41 @@ +use stackable_versioned::versioned; +// --- +#[versioned( + version(name = "v1alpha1"), + version( + name = "v1beta1", + doc = r#" + Additional docs for this version which are purposefully long to + show how manual line wrapping works. \ + Multi-line docs are also supported, as per regular doc-comments. + "# + ), + version(name = "v1beta2"), + version(name = "v1"), + version(name = "v2"), + options(k8s(experimental_conversion_tracking)) +)] +// --- +mod versioned { + /// Test + #[derive(Default)] + struct Foo { + /// This field is available in every version (so far). + foo: String, + + /// Keep the main field docs the same, even after the field is deprecated. + #[versioned(deprecated(since = "v1beta1", note = "gone"))] + deprecated_bar: String, + + /// This is for baz + #[versioned(added(since = "v1beta1"))] + baz: String, + + /// This is will keep changing over time. + #[versioned(changed(since = "v1beta1", from_name = "qoox"))] + #[versioned(changed(since = "v1", from_name = "qaax"))] + quux: String, + } +} +// --- +fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/pass/downgrade_with.rs b/crates/stackable-versioned-macros/tests/inputs/pass/downgrade_with.rs new file mode 100644 index 000000000..eddb3a609 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/pass/downgrade_with.rs @@ -0,0 +1,26 @@ +use stackable_versioned::versioned; +// --- +#[versioned(version(name = "v1alpha1"), version(name = "v1"), version(name = "v2"))] +// --- +mod versioned { + struct Foo { + #[versioned( + // This tests two additional things: + // - that both unquoted and quoted usage works + // - that the renamed name does get picked up correctly by the conversion function + changed(since = "v1", from_type = "u16", from_name = "bar", downgrade_with = u32_to_u16), + changed(since = "v2", from_type = "u32", downgrade_with = "u64_to_u32") + )] + baz: u64, + } +} +// --- +fn main() {} + +fn u32_to_u16(input: u32) -> u16 { + input.try_into().unwrap() +} + +fn u64_to_u32(input: u64) -> u32 { + input.try_into().unwrap() +} diff --git a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/module.rs b/crates/stackable-versioned-macros/tests/inputs/pass/module.rs similarity index 89% rename from crates/stackable-versioned-macros/tests/inputs/k8s/pass/module.rs rename to crates/stackable-versioned-macros/tests/inputs/pass/module.rs index d633bbdb0..c0c60e4a5 100644 --- a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/module.rs +++ b/crates/stackable-versioned-macros/tests/inputs/pass/module.rs @@ -13,7 +13,7 @@ pub(crate) mod versioned { boom: Option, } - #[versioned(k8s(group = "foo.example.org", plural = "foos", namespaced))] + #[versioned(crd(group = "foo.example.org", plural = "foos", namespaced))] #[derive( Clone, Debug, @@ -32,7 +32,7 @@ pub(crate) mod versioned { deprecated_foo: String, } - #[versioned(k8s(group = "bar.example.org", plural = "bars"))] + #[versioned(crd(group = "bar.example.org", plural = "bars"))] #[derive( Clone, Debug, diff --git a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/module_preserve.rs b/crates/stackable-versioned-macros/tests/inputs/pass/module_preserve.rs similarity index 89% rename from crates/stackable-versioned-macros/tests/inputs/k8s/pass/module_preserve.rs rename to crates/stackable-versioned-macros/tests/inputs/pass/module_preserve.rs index 3fdf905a8..c7160d343 100644 --- a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/module_preserve.rs +++ b/crates/stackable-versioned-macros/tests/inputs/pass/module_preserve.rs @@ -14,7 +14,7 @@ pub(crate) mod versioned { boom: Option, } - #[versioned(k8s(group = "foo.example.org", plural = "foos", namespaced))] + #[versioned(crd(group = "foo.example.org", plural = "foos", namespaced))] #[derive( Clone, Debug, @@ -33,7 +33,7 @@ pub(crate) mod versioned { deprecated_foo: String, } - #[versioned(k8s(group = "bar.example.org", plural = "bars"))] + #[versioned(crd(group = "bar.example.org", plural = "bars"))] #[derive( Clone, Debug, diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/skip_from_module.rs b/crates/stackable-versioned-macros/tests/inputs/pass/renamed_field.rs similarity index 60% rename from crates/stackable-versioned-macros/tests/inputs/default/pass/skip_from_module.rs rename to crates/stackable-versioned-macros/tests/inputs/pass/renamed_field.rs index e5d033ac3..f8c78e479 100644 --- a/crates/stackable-versioned-macros/tests/inputs/default/pass/skip_from_module.rs +++ b/crates/stackable-versioned-macros/tests/inputs/pass/renamed_field.rs @@ -3,14 +3,13 @@ use stackable_versioned::versioned; #[versioned( version(name = "v1alpha1"), version(name = "v1beta1"), - options(skip(from)) + version(name = "v1") )] // --- -pub(crate) mod versioned { - pub struct Foo { +mod versioned { + struct Foo { + #[versioned(changed(since = "v1beta1", from_name = "bat"))] bar: usize, - - #[versioned(added(since = "v1beta1"))] baz: bool, } } diff --git a/crates/stackable-versioned-macros/tests/inputs/pass/renamed_kind.rs b/crates/stackable-versioned-macros/tests/inputs/pass/renamed_kind.rs new file mode 100644 index 000000000..7e25a2793 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/pass/renamed_kind.rs @@ -0,0 +1,26 @@ +use stackable_versioned::versioned; +// --- +#[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + version(name = "v1") +)] +// --- +pub mod versioned { + #[versioned(crd(group = "stackable.tech", kind = "FooBar", namespaced))] + #[derive( + Clone, + Debug, + serde::Deserialize, + serde::Serialize, + schemars::JsonSchema, + kube::CustomResource, + )] + pub struct FooSpec { + #[versioned(added(since = "v1beta1"), changed(since = "v1", from_name = "bah"))] + bar: usize, + baz: bool, + } +} +// --- +fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/pass/shortnames.rs b/crates/stackable-versioned-macros/tests/inputs/pass/shortnames.rs new file mode 100644 index 000000000..87b85503c --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/pass/shortnames.rs @@ -0,0 +1,18 @@ +use stackable_versioned::versioned; +// --- +#[versioned(version(name = "v1alpha1"))] +// --- +pub(crate) mod versioned { + #[versioned(crd(group = "stackable.tech", shortname = "f", shortname = "fo"))] + #[derive( + Clone, + Debug, + serde::Deserialize, + serde::Serialize, + schemars::JsonSchema, + kube::CustomResource, + )] + pub(crate) struct FooSpec {} +} +// --- +fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/default/pass/submodule.rs b/crates/stackable-versioned-macros/tests/inputs/pass/submodule.rs similarity index 100% rename from crates/stackable-versioned-macros/tests/inputs/default/pass/submodule.rs rename to crates/stackable-versioned-macros/tests/inputs/pass/submodule.rs diff --git a/crates/stackable-versioned-macros/tests/trybuild.rs b/crates/stackable-versioned-macros/tests/trybuild.rs index a8e6388ff..b9c0fd343 100644 --- a/crates/stackable-versioned-macros/tests/trybuild.rs +++ b/crates/stackable-versioned-macros/tests/trybuild.rs @@ -16,64 +16,34 @@ // again but before running tests, otherwise compilation will fail (as expected). #[allow(dead_code)] mod inputs { - mod default { - mod pass { - // mod attribute_enum; - // mod attribute_struct; - // mod basic_struct; - // mod deprecate_enum; - // mod deprecate_struct; - // mod downgrade_with; - // mod enum_data_simple; - // mod generics_defaults; - // mod generics_module; - // mod generics_struct; - // mod module; - // mod module_preserve; - // mod rename; - // mod skip_from_for_version; - // mod skip_from_module; - // mod skip_from_module_for_version; - // mod submodule; - } - mod fail { - // mod changed; - // mod deprecate; - // mod skip_from_all; - // mod skip_from_version; - // mod submodule_invalid_name; - // mod submodule_use_statement; - } + mod pass { + // mod added; + // mod basic; + // mod conversion_tracking; + // mod crate_overrides; + // mod docs; + // mod downgrade_with; + // mod module; + // mod module_preserve; + // mod renamed_field; + // mod renamed_kind; + // mod shortnames; + // mod submodule; } - mod k8s { - mod pass { - // mod basic; - // mod conversion_tracking; - // mod crate_overrides; - // mod module; - // mod module_preserve; - // mod renamed_kind; - // mod shortnames; - // mod skip; - } - - mod fail { - // mod crd; - } + mod fail { + // mod applied_to_struct; + // mod changed; + // mod deprecate; + // mod spec_suffix; + // mod unknown_version; + // mod submodule_invalid_name; } } #[test] -fn default() { - let t = trybuild::TestCases::new(); - t.compile_fail("tests/inputs/default/fail/*.rs"); - t.pass("tests/inputs/default/pass/*.rs"); -} - -#[test] -fn k8s() { +fn ui() { let t = trybuild::TestCases::new(); - t.compile_fail("tests/inputs/k8s/fail/*.rs"); - t.pass("tests/inputs/k8s/pass/*.rs"); + t.pass("tests/inputs/pass/*.rs"); + t.compile_fail("tests/inputs/fail/*.rs"); } From e8c0d93b445a8fc09bac9f777a95866bedf1eec1 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 4 Jul 2025 11:48:13 +0200 Subject: [PATCH 09/16] fix(stackable-versioned): Use trait from core, use correct boolean expr --- .../stackable-versioned-macros/src/codegen/container/enum.rs | 2 +- .../src/codegen/container/struct/conversion.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/stackable-versioned-macros/src/codegen/container/enum.rs b/crates/stackable-versioned-macros/src/codegen/container/enum.rs index 8a85e4cef..fbe5b40b7 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/enum.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/enum.rs @@ -180,7 +180,7 @@ impl Enum { quote! { #automatically_derived #allow_attribute - impl #impl_generics ::std::convert::From<#from_module_ident::#enum_ident #type_generics> for #for_module_ident::#enum_ident #type_generics + impl #impl_generics ::core::convert::From<#from_module_ident::#enum_ident #type_generics> for #for_module_ident::#enum_ident #type_generics #where_clause { fn from(#from_enum_ident: #from_module_ident::#enum_ident #type_generics) -> Self { diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs index 524951de2..d7026bb9a 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs @@ -101,7 +101,7 @@ impl Struct { quote! { #automatically_derived - impl ::std::convert::From<#from_module_ident::#object_struct_ident> for #for_module_ident::#object_struct_ident { + impl ::core::convert::From<#from_module_ident::#object_struct_ident> for #for_module_ident::#object_struct_ident { fn from(#from_struct_parameter_ident: #from_module_ident::#object_struct_ident) -> Self { #from_inner } @@ -551,7 +551,7 @@ impl Struct { field: "kind".to_owned() })?; - if kind == #enum_ident_string { + if kind != #enum_ident_string { return Err(#parse_object_error::UnexpectedKind{ kind: kind.to_owned(), expected: #enum_ident_string.to_owned(), From 36ecc2066212368458682010ffc81377f71da96f Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 4 Jul 2025 11:49:30 +0200 Subject: [PATCH 10/16] fix(stackable-versioned): Remove minor errors in generated code --- .../src/codegen/container/struct/conversion.rs | 6 +++--- .../src/codegen/container/struct/mod.rs | 4 ++-- crates/stackable-versioned-macros/src/codegen/module.rs | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs index d7026bb9a..4911a19ac 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs @@ -432,7 +432,7 @@ impl Struct { for object in objects { // This clone is required because in the noop case we move the object into // the converted objects vec. - let current_object = Self::from_json_value(object.clone()) + let current_object = Self::from_json_object(object.clone()) .map_err(|source| #convert_object_error::Parse { source })?; match (current_object, desired_api_version) { @@ -513,7 +513,7 @@ impl Struct { }) } - pub(super) fn generate_from_json_value_fn( + pub(super) fn generate_from_json_object_fn( &self, mod_gen_ctx: ModuleGenerationContext<'_>, spec_gen_ctx: &SpecGenerationContext<'_>, @@ -540,7 +540,7 @@ impl Struct { }); Some(quote! { - fn from_json_value(object_value: #serde_json_path::Value) -> ::std::result::Result { + fn from_json_object(object_value: #serde_json_path::Value) -> ::std::result::Result { let kind = object_value .get("kind") .ok_or_else(|| #parse_object_error::FieldNotPresent { diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs index 4b891ca2a..1fada95f1 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs @@ -318,7 +318,7 @@ impl Struct { let merged_crd_fn = self.generate_merged_crd_fn(mod_gen_ctx, spec_gen_ctx); let try_convert_fn = self.generate_try_convert_fn(versions, mod_gen_ctx, spec_gen_ctx); - let from_json_value_fn = self.generate_from_json_value_fn(mod_gen_ctx, spec_gen_ctx); + let from_json_object_fn = self.generate_from_json_object_fn(mod_gen_ctx, spec_gen_ctx); let into_json_value_fn = self.generate_into_json_value_fn(mod_gen_ctx, spec_gen_ctx); let automatically_derived = mod_gen_ctx.automatically_derived_attr(); @@ -328,7 +328,7 @@ impl Struct { impl #enum_ident { #merged_crd_fn #try_convert_fn - #from_json_value_fn + #from_json_object_fn #into_json_value_fn } } diff --git a/crates/stackable-versioned-macros/src/codegen/module.rs b/crates/stackable-versioned-macros/src/codegen/module.rs index 779653690..d9680c6f4 100644 --- a/crates/stackable-versioned-macros/src/codegen/module.rs +++ b/crates/stackable-versioned-macros/src/codegen/module.rs @@ -137,7 +137,6 @@ impl Module { } let preserve_module = self.options.common.preserve_module.is_present(); - let allow_unsorted = self.options.common.allow_unsorted.is_present(); let module_ident = &self.ident; let module_vis = &self.vis; @@ -146,7 +145,7 @@ impl Module { // of version modules (eg. 'v1alpha1') to be public, so that they are accessible inside the // preserved (wrapping) module. Otherwise, we can inherit the visibility from the module // which will be erased. - let version_module_vis = if allow_unsorted { + let version_module_vis = if preserve_module { &Visibility::Public(Pub::default()) } else { &self.vis From 88d9050e4c48d6f3db149b3d24ecc1c7b6c0bb93 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 4 Jul 2025 11:50:18 +0200 Subject: [PATCH 11/16] test(stackable-version): Review and accept updated snapshots --- crates/stackable-versioned-macros/src/lib.rs | 20 +- ...shot_tests__default@attribute_enum.rs.snap | 95 ------- ...ot_tests__default@attribute_struct.rs.snap | 90 ------- ...apshot_tests__default@basic_struct.rs.snap | 139 ---------- ...shot_tests__default@deprecate_enum.rs.snap | 126 --------- ...ot_tests__default@deprecate_struct.rs.snap | 126 --------- ...ot_tests__default@enum_data_simple.rs.snap | 39 --- ...t_tests__default@generics_defaults.rs.snap | 51 ---- ...pshot_tests__default@generics_enum.rs.snap | 51 ---- ...hot_tests__default@generics_module.rs.snap | 86 ------ ...hot_tests__default@generics_struct.rs.snap | 51 ---- ...os__snapshot_tests__default@module.rs.snap | 106 -------- ...hot_tests__default@module_preserve.rs.snap | 98 ------- ...sts__default@skip_from_for_version.rs.snap | 44 ---- ...ot_tests__default@skip_from_module.rs.snap | 20 -- ...fault@skip_from_module_for_version.rs.snap | 69 ----- ...ned_macros__snapshots__pass@added.rs.snap} | 14 +- ...ned_macros__snapshots__pass@basic.rs.snap} | 119 +++++---- ...apshots__pass@conversion_tracking.rs.snap} | 218 +++++++++++----- ...__snapshots__pass@crate_overrides.rs.snap} | 109 ++++---- ...ioned_macros__snapshots__pass@docs.rs.snap | 247 ++++++++++++++++++ ...s__snapshots__pass@downgrade_with.rs.snap} | 10 +- ...ed_macros__snapshots__pass@module.rs.snap} | 232 +++++++++------- ...__snapshots__pass@module_preserve.rs.snap} | 224 +++++++++------- ...os__snapshots__pass@renamed_field.rs.snap} | 10 +- ...ros__snapshots__pass@renamed_kind.rs.snap} | 109 ++++---- ...acros__snapshots__pass@shortnames.rs.snap} | 19 +- ...macros__snapshots__pass@submodule.rs.snap} | 6 +- 28 files changed, 897 insertions(+), 1631 deletions(-) delete mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@attribute_enum.rs.snap delete mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@attribute_struct.rs.snap delete mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@basic_struct.rs.snap delete mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@deprecate_enum.rs.snap delete mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@deprecate_struct.rs.snap delete mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@enum_data_simple.rs.snap delete mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_defaults.rs.snap delete mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_enum.rs.snap delete mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_module.rs.snap delete mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_struct.rs.snap delete mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@module.rs.snap delete mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@module_preserve.rs.snap delete mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@skip_from_for_version.rs.snap delete mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@skip_from_module.rs.snap delete mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@skip_from_module_for_version.rs.snap rename crates/stackable-versioned-macros/tests/snapshots/{stackable_versioned_macros__snapshot_tests__default@added.rs.snap => stackable_versioned_macros__snapshots__pass@added.rs.snap} (82%) rename crates/stackable-versioned-macros/tests/snapshots/{stackable_versioned_macros__snapshot_tests__k8s@basic.rs.snap => stackable_versioned_macros__snapshots__pass@basic.rs.snap} (82%) rename crates/stackable-versioned-macros/tests/snapshots/{stackable_versioned_macros__snapshot_tests__k8s@conversion_tracking.rs.snap => stackable_versioned_macros__snapshots__pass@conversion_tracking.rs.snap} (68%) rename crates/stackable-versioned-macros/tests/snapshots/{stackable_versioned_macros__snapshot_tests__k8s@crate_overrides.rs.snap => stackable_versioned_macros__snapshots__pass@crate_overrides.rs.snap} (83%) create mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@docs.rs.snap rename crates/stackable-versioned-macros/tests/snapshots/{stackable_versioned_macros__snapshot_tests__default@downgrade_with.rs.snap => stackable_versioned_macros__snapshots__pass@downgrade_with.rs.snap} (75%) rename crates/stackable-versioned-macros/tests/snapshots/{stackable_versioned_macros__snapshot_tests__k8s@module.rs.snap => stackable_versioned_macros__snapshots__pass@module.rs.snap} (83%) rename crates/stackable-versioned-macros/tests/snapshots/{stackable_versioned_macros__snapshot_tests__k8s@module_preserve.rs.snap => stackable_versioned_macros__snapshots__pass@module_preserve.rs.snap} (83%) rename crates/stackable-versioned-macros/tests/snapshots/{stackable_versioned_macros__snapshot_tests__default@rename.rs.snap => stackable_versioned_macros__snapshots__pass@renamed_field.rs.snap} (79%) rename crates/stackable-versioned-macros/tests/snapshots/{stackable_versioned_macros__snapshot_tests__k8s@renamed_kind.rs.snap => stackable_versioned_macros__snapshots__pass@renamed_kind.rs.snap} (82%) rename crates/stackable-versioned-macros/tests/snapshots/{stackable_versioned_macros__snapshot_tests__k8s@shortnames.rs.snap => stackable_versioned_macros__snapshots__pass@shortnames.rs.snap} (94%) rename crates/stackable-versioned-macros/tests/snapshots/{stackable_versioned_macros__snapshot_tests__default@submodule.rs.snap => stackable_versioned_macros__snapshots__pass@submodule.rs.snap} (75%) diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs index 0783a4429..5acdde091 100644 --- a/crates/stackable-versioned-macros/src/lib.rs +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -1028,29 +1028,17 @@ where } #[cfg(test)] -mod snapshot_tests { +mod snapshots { use insta::{assert_snapshot, glob}; use super::*; - // TODO (@Techassi): Combine tests, there are no default/k8s-specific tests anymore #[test] - fn default() { + fn pass() { + // TODO (@Techassi): Re-add skip tests let _settings_guard = test_utils::set_snapshot_path().bind_to_scope(); - glob!("../tests/inputs/default/pass", "*.rs", |path| { - let formatted = test_utils::expand_from_file(path) - .inspect_err(|err| eprintln!("{err}")) - .unwrap(); - assert_snapshot!(formatted); - }); - } - - #[test] - fn k8s() { - let _settings_guard = test_utils::set_snapshot_path().bind_to_scope(); - - glob!("../tests/inputs/k8s/pass", "*.rs", |path| { + glob!("../tests/inputs/pass", "*.rs", |path| { let formatted = test_utils::expand_from_file(path) .inspect_err(|err| eprintln!("{err}")) .unwrap(); diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@attribute_enum.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@attribute_enum.rs.snap deleted file mode 100644 index d2ae4b57b..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@attribute_enum.rs.snap +++ /dev/null @@ -1,95 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/attribute_enum.rs ---- -#[automatically_derived] -mod v1alpha1 { - use super::*; - #[derive(Default)] - pub enum Foo { - /// This variant is available in every version (so far). - #[default] - Foo, - /// Keep the main field docs the same, even after the field is - /// deprecated. - Bar, - /// This is will keep changing over time. - Qoox, - } -} -#[automatically_derived] -mod v1beta1 { - use super::*; - ///Additional docs for this version which are purposefully long to - ///show how manual line wrapping works. \ - ///Multi-line docs are also supported, as per regular doc-comments. - #[derive(Default)] - pub enum Foo { - /// This variant is available in every version (so far). - #[default] - Foo, - /// Keep the main field docs the same, even after the field is - /// deprecated. - #[deprecated = "gone"] - DeprecatedBar, - /// This is for baz - Baz, - /// This is will keep changing over time. - Qaax, - } -} -#[automatically_derived] -mod v1beta2 { - use super::*; - #[derive(Default)] - pub enum Foo { - /// This variant is available in every version (so far). - #[default] - Foo, - /// Keep the main field docs the same, even after the field is - /// deprecated. - #[deprecated] - DeprecatedBar, - /// This is for baz - Baz, - /// This is will keep changing over time. - Qaax, - } -} -#[automatically_derived] -mod v1 { - use super::*; - #[derive(Default)] - pub enum Foo { - /// This variant is available in every version (so far). - #[default] - Foo, - /// Keep the main field docs the same, even after the field is - /// deprecated. - #[deprecated] - DeprecatedBar, - /// This is for baz - Baz, - /// This is will keep changing over time. - Quux, - } -} -#[automatically_derived] -mod v2 { - use super::*; - #[derive(Default)] - pub enum Foo { - /// This variant is available in every version (so far). - #[default] - Foo, - /// Keep the main field docs the same, even after the field is - /// deprecated. - #[deprecated] - DeprecatedBar, - /// This is for baz - Baz, - /// This is will keep changing over time. - Quux, - } -} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@attribute_struct.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@attribute_struct.rs.snap deleted file mode 100644 index d4c560ee1..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@attribute_struct.rs.snap +++ /dev/null @@ -1,90 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/attribute_struct.rs ---- -#[automatically_derived] -mod v1alpha1 { - use super::*; - /// Test - #[derive(Default)] - pub struct Foo { - /// This field is available in every version (so far). - pub foo: String, - /// Keep the main field docs the same, even after the field is deprecated. - pub bar: String, - /// This is will keep changing over time. - pub qoox: String, - } -} -#[automatically_derived] -mod v1beta1 { - use super::*; - ///Additional docs for this version which are purposefully long to - ///show how manual line wrapping works. \ - ///Multi-line docs are also supported, as per regular doc-comments. - /// Test - #[derive(Default)] - pub struct Foo { - /// This field is available in every version (so far). - pub foo: String, - /// Keep the main field docs the same, even after the field is deprecated. - #[deprecated = "gone"] - pub deprecated_bar: String, - /// This is for baz - pub baz: String, - /// This is will keep changing over time. - pub qaax: String, - } -} -#[automatically_derived] -mod v1beta2 { - use super::*; - /// Test - #[derive(Default)] - pub struct Foo { - /// This field is available in every version (so far). - pub foo: String, - /// Keep the main field docs the same, even after the field is deprecated. - #[deprecated] - pub deprecated_bar: String, - /// This is for baz - pub baz: String, - /// This is will keep changing over time. - pub qaax: String, - } -} -#[automatically_derived] -mod v1 { - use super::*; - /// Test - #[derive(Default)] - pub struct Foo { - /// This field is available in every version (so far). - pub foo: String, - /// Keep the main field docs the same, even after the field is deprecated. - #[deprecated] - pub deprecated_bar: String, - /// This is for baz - pub baz: String, - /// This is will keep changing over time. - pub quux: String, - } -} -#[automatically_derived] -mod v2 { - use super::*; - /// Test - #[derive(Default)] - pub struct Foo { - /// This field is available in every version (so far). - pub foo: String, - /// Keep the main field docs the same, even after the field is deprecated. - #[deprecated] - pub deprecated_bar: String, - /// This is for baz - pub baz: String, - /// This is will keep changing over time. - pub quux: String, - } -} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@basic_struct.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@basic_struct.rs.snap deleted file mode 100644 index 1e8601603..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@basic_struct.rs.snap +++ /dev/null @@ -1,139 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/basic_struct.rs ---- -#[automatically_derived] -#[deprecated = "Version v1alpha1 is deprecated"] -pub(crate) mod v1alpha1 { - use super::*; - pub struct Foo { - /// Test - pub jjj: u8, - pub baz: bool, - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v1beta1::Foo { - fn from(__sv_foo: v1alpha1::Foo) -> Self { - Self { - bar: __sv_foo.jjj.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v1alpha1::Foo { - fn from(__sv_foo: v1beta1::Foo) -> Self { - Self { - jjj: u16_to_u8(__sv_foo.bar), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -pub(crate) mod v1beta1 { - use super::*; - pub struct Foo { - /// Test - pub bar: u16, - pub baz: bool, - } -} -#[automatically_derived] -impl ::std::convert::From for v1::Foo { - fn from(__sv_foo: v1beta1::Foo) -> Self { - Self { - foo: ::std::default::Default::default(), - bar: __sv_foo.bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -impl ::std::convert::From for v1beta1::Foo { - fn from(__sv_foo: v1::Foo) -> Self { - Self { - bar: usize_to_u16(__sv_foo.bar), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -pub(crate) mod v1 { - use super::*; - pub struct Foo { - pub foo: String, - /// Test - pub bar: usize, - pub baz: bool, - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v2::Foo { - fn from(__sv_foo: v1::Foo) -> Self { - Self { - foo: __sv_foo.foo.into(), - deprecated_bar: __sv_foo.bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v1::Foo { - fn from(__sv_foo: v2::Foo) -> Self { - Self { - foo: __sv_foo.foo.into(), - bar: __sv_foo.deprecated_bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -pub(crate) mod v2 { - use super::*; - pub struct Foo { - pub foo: String, - /// Test - #[deprecated = "not empty"] - pub deprecated_bar: usize, - pub baz: bool, - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v3::Foo { - fn from(__sv_foo: v2::Foo) -> Self { - Self { - foo: __sv_foo.foo.into(), - deprecated_bar: __sv_foo.deprecated_bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v2::Foo { - fn from(__sv_foo: v3::Foo) -> Self { - Self { - foo: __sv_foo.foo.into(), - deprecated_bar: __sv_foo.deprecated_bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -pub(crate) mod v3 { - use super::*; - pub struct Foo { - pub foo: String, - /// Test - #[deprecated] - pub deprecated_bar: usize, - pub baz: bool, - } -} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@deprecate_enum.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@deprecate_enum.rs.snap deleted file mode 100644 index 274de6173..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@deprecate_enum.rs.snap +++ /dev/null @@ -1,126 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/deprecate_enum.rs ---- -#[automatically_derived] -mod v1alpha1 { - use super::*; - pub enum Foo { - Bar, - Baz, - } -} -#[automatically_derived] -impl ::std::convert::From for v1beta1::Foo { - fn from(__sv_foo: v1alpha1::Foo) -> Self { - match __sv_foo { - v1alpha1::Foo::Bar => v1beta1::Foo::Bar, - v1alpha1::Foo::Baz => v1beta1::Foo::Baz, - } - } -} -#[automatically_derived] -impl ::std::convert::From for v1alpha1::Foo { - fn from(__sv_foo: v1beta1::Foo) -> Self { - match __sv_foo { - v1beta1::Foo::Bar => v1alpha1::Foo::Bar, - v1beta1::Foo::Baz => v1alpha1::Foo::Baz, - } - } -} -#[automatically_derived] -mod v1beta1 { - use super::*; - pub enum Foo { - Bar, - Baz, - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v1::Foo { - fn from(__sv_foo: v1beta1::Foo) -> Self { - match __sv_foo { - v1beta1::Foo::Bar => v1::Foo::DeprecatedBar, - v1beta1::Foo::Baz => v1::Foo::Baz, - } - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v1beta1::Foo { - fn from(__sv_foo: v1::Foo) -> Self { - match __sv_foo { - v1::Foo::DeprecatedBar => v1beta1::Foo::Bar, - v1::Foo::Baz => v1beta1::Foo::Baz, - } - } -} -#[automatically_derived] -mod v1 { - use super::*; - pub enum Foo { - #[deprecated] - DeprecatedBar, - Baz, - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v2::Foo { - fn from(__sv_foo: v1::Foo) -> Self { - match __sv_foo { - v1::Foo::DeprecatedBar => v2::Foo::DeprecatedBar, - v1::Foo::Baz => v2::Foo::Baz, - } - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v1::Foo { - fn from(__sv_foo: v2::Foo) -> Self { - match __sv_foo { - v2::Foo::DeprecatedBar => v1::Foo::DeprecatedBar, - v2::Foo::Baz => v1::Foo::Baz, - } - } -} -#[automatically_derived] -mod v2 { - use super::*; - pub enum Foo { - #[deprecated] - DeprecatedBar, - Baz, - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v3::Foo { - fn from(__sv_foo: v2::Foo) -> Self { - match __sv_foo { - v2::Foo::DeprecatedBar => v3::Foo::DeprecatedBar, - v2::Foo::Baz => v3::Foo::Baz, - } - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v2::Foo { - fn from(__sv_foo: v3::Foo) -> Self { - match __sv_foo { - v3::Foo::DeprecatedBar => v2::Foo::DeprecatedBar, - v3::Foo::Baz => v2::Foo::Baz, - } - } -} -#[automatically_derived] -mod v3 { - use super::*; - pub enum Foo { - #[deprecated] - DeprecatedBar, - Baz, - } -} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@deprecate_struct.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@deprecate_struct.rs.snap deleted file mode 100644 index 3159caee8..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@deprecate_struct.rs.snap +++ /dev/null @@ -1,126 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/deprecate_struct.rs ---- -#[automatically_derived] -mod v1alpha1 { - use super::*; - pub struct Foo { - pub bar: usize, - pub baz: bool, - } -} -#[automatically_derived] -impl ::std::convert::From for v1beta1::Foo { - fn from(__sv_foo: v1alpha1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -impl ::std::convert::From for v1alpha1::Foo { - fn from(__sv_foo: v1beta1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -mod v1beta1 { - use super::*; - pub struct Foo { - pub bar: usize, - pub baz: bool, - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v1::Foo { - fn from(__sv_foo: v1beta1::Foo) -> Self { - Self { - deprecated_bar: __sv_foo.bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v1beta1::Foo { - fn from(__sv_foo: v1::Foo) -> Self { - Self { - bar: __sv_foo.deprecated_bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -mod v1 { - use super::*; - pub struct Foo { - #[deprecated = "gone"] - pub deprecated_bar: usize, - pub baz: bool, - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v2::Foo { - fn from(__sv_foo: v1::Foo) -> Self { - Self { - deprecated_bar: __sv_foo.deprecated_bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v1::Foo { - fn from(__sv_foo: v2::Foo) -> Self { - Self { - deprecated_bar: __sv_foo.deprecated_bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -mod v2 { - use super::*; - pub struct Foo { - #[deprecated] - pub deprecated_bar: usize, - pub baz: bool, - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v3::Foo { - fn from(__sv_foo: v2::Foo) -> Self { - Self { - deprecated_bar: __sv_foo.deprecated_bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v2::Foo { - fn from(__sv_foo: v3::Foo) -> Self { - Self { - deprecated_bar: __sv_foo.deprecated_bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -mod v3 { - use super::*; - pub struct Foo { - #[deprecated] - pub deprecated_bar: usize, - pub baz: bool, - } -} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@enum_data_simple.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@enum_data_simple.rs.snap deleted file mode 100644 index 572d6639c..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@enum_data_simple.rs.snap +++ /dev/null @@ -1,39 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/enum_data_simple.rs ---- -#[automatically_derived] -mod v1alpha1 { - use super::*; - pub enum Foo { - Foo, - Bar(u32, String), - } -} -#[automatically_derived] -impl ::std::convert::From for v1alpha2::Foo { - fn from(__sv_foo: v1alpha1::Foo) -> Self { - match __sv_foo { - v1alpha1::Foo::Foo => v1alpha2::Foo::Foo, - v1alpha1::Foo::Bar(__sv_0, __sv_1) => v1alpha2::Foo::Bar(__sv_0, __sv_1), - } - } -} -#[automatically_derived] -impl ::std::convert::From for v1alpha1::Foo { - fn from(__sv_foo: v1alpha2::Foo) -> Self { - match __sv_foo { - v1alpha2::Foo::Foo => v1alpha1::Foo::Foo, - v1alpha2::Foo::Bar(__sv_0, __sv_1) => v1alpha1::Foo::Bar(__sv_0, __sv_1), - } - } -} -#[automatically_derived] -mod v1alpha2 { - use super::*; - pub enum Foo { - Foo, - Bar(u32, String), - } -} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_defaults.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_defaults.rs.snap deleted file mode 100644 index 94286a5f8..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_defaults.rs.snap +++ /dev/null @@ -1,51 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/generics_defaults.rs ---- -#[automatically_derived] -pub mod v1alpha1 { - use super::*; - pub struct Foo - where - T: Default, - { - pub bar: T, - pub baz: u8, - } -} -#[automatically_derived] -impl ::std::convert::From> for v1::Foo -where - T: Default, -{ - fn from(__sv_foo: v1alpha1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -impl ::std::convert::From> for v1alpha1::Foo -where - T: Default, -{ - fn from(__sv_foo: v1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -pub mod v1 { - use super::*; - pub struct Foo - where - T: Default, - { - pub bar: T, - pub baz: u8, - } -} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_enum.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_enum.rs.snap deleted file mode 100644 index dbaf91fcb..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_enum.rs.snap +++ /dev/null @@ -1,51 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/generics_enum.rs ---- -#[automatically_derived] -pub mod v1alpha1 { - use super::*; - pub enum Foo - where - T: Default, - { - Bar(T), - Baz, - } -} -#[automatically_derived] -impl ::std::convert::From> for v1::Foo -where - T: Default, -{ - fn from(__sv_foo: v1alpha1::Foo) -> Self { - match __sv_foo { - v1alpha1::Foo::Bar(__sv_0) => v1::Foo::Bar(__sv_0), - v1alpha1::Foo::Baz => v1::Foo::Baz, - } - } -} -#[automatically_derived] -impl ::std::convert::From> for v1alpha1::Foo -where - T: Default, -{ - fn from(__sv_foo: v1::Foo) -> Self { - match __sv_foo { - v1::Foo::Bar(__sv_0) => v1alpha1::Foo::Bar(__sv_0), - v1::Foo::Baz => v1alpha1::Foo::Baz, - } - } -} -#[automatically_derived] -pub mod v1 { - use super::*; - pub enum Foo - where - T: Default, - { - Bar(T), - Baz, - } -} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_module.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_module.rs.snap deleted file mode 100644 index 68b14527f..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_module.rs.snap +++ /dev/null @@ -1,86 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/generics_module.rs ---- -#[automatically_derived] -pub mod versioned { - pub mod v1alpha1 { - use super::*; - pub struct Foo - where - T: Default, - { - pub bar: T, - pub baz: u8, - } - pub enum Boom - where - T: Default, - { - Big(T), - Shaq, - } - } - impl ::std::convert::From> for v1::Foo - where - T: Default, - { - fn from(__sv_foo: v1alpha1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - baz: __sv_foo.baz.into(), - } - } - } - impl ::std::convert::From> for v1alpha1::Foo - where - T: Default, - { - fn from(__sv_foo: v1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - baz: __sv_foo.baz.into(), - } - } - } - impl ::std::convert::From> for v1::Boom - where - T: Default, - { - fn from(__sv_boom: v1alpha1::Boom) -> Self { - match __sv_boom { - v1alpha1::Boom::Big(__sv_0) => v1::Boom::Big(__sv_0), - v1alpha1::Boom::Shaq => v1::Boom::Shaq, - } - } - } - impl ::std::convert::From> for v1alpha1::Boom - where - T: Default, - { - fn from(__sv_boom: v1::Boom) -> Self { - match __sv_boom { - v1::Boom::Big(__sv_0) => v1alpha1::Boom::Big(__sv_0), - v1::Boom::Shaq => v1alpha1::Boom::Shaq, - } - } - } - pub mod v1 { - use super::*; - pub struct Foo - where - T: Default, - { - pub bar: T, - pub baz: u8, - } - pub enum Boom - where - T: Default, - { - Big(T), - Shaq, - } - } -} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_struct.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_struct.rs.snap deleted file mode 100644 index af5daf229..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@generics_struct.rs.snap +++ /dev/null @@ -1,51 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/generics_struct.rs ---- -#[automatically_derived] -pub mod v1alpha1 { - use super::*; - pub struct Foo - where - T: Default, - { - pub bar: T, - pub baz: u8, - } -} -#[automatically_derived] -impl ::std::convert::From> for v1::Foo -where - T: Default, -{ - fn from(__sv_foo: v1alpha1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -impl ::std::convert::From> for v1alpha1::Foo -where - T: Default, -{ - fn from(__sv_foo: v1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -pub mod v1 { - use super::*; - pub struct Foo - where - T: Default, - { - pub bar: T, - pub baz: u8, - } -} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@module.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@module.rs.snap deleted file mode 100644 index 3f4ed5ff3..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@module.rs.snap +++ /dev/null @@ -1,106 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/module.rs ---- -#[automatically_derived] -pub(crate) mod v1alpha1 { - use super::*; - pub struct Foo { - pub bar: usize, - pub foo: String, - } - pub struct Bar { - pub baz: String, - } -} -#[automatically_derived] -impl ::std::convert::From for v1::Foo { - fn from(__sv_foo: v1alpha1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - baz: ::std::default::Default::default(), - foo: __sv_foo.foo.into(), - } - } -} -#[automatically_derived] -impl ::std::convert::From for v1alpha1::Foo { - fn from(__sv_foo: v1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - foo: __sv_foo.foo.into(), - } - } -} -#[automatically_derived] -impl ::std::convert::From for v1::Bar { - fn from(__sv_bar: v1alpha1::Bar) -> Self { - Self { baz: __sv_bar.baz.into() } - } -} -#[automatically_derived] -impl ::std::convert::From for v1alpha1::Bar { - fn from(__sv_bar: v1::Bar) -> Self { - Self { baz: __sv_bar.baz.into() } - } -} -#[automatically_derived] -pub(crate) mod v1 { - use super::*; - pub struct Foo { - pub bar: usize, - pub baz: bool, - pub foo: String, - } - pub struct Bar { - pub baz: String, - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v2alpha1::Foo { - fn from(__sv_foo: v1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - baz: __sv_foo.baz.into(), - deprecated_foo: __sv_foo.foo.into(), - } - } -} -#[automatically_derived] -#[allow(deprecated)] -impl ::std::convert::From for v1::Foo { - fn from(__sv_foo: v2alpha1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - baz: __sv_foo.baz.into(), - foo: __sv_foo.deprecated_foo.into(), - } - } -} -#[automatically_derived] -impl ::std::convert::From for v2alpha1::Bar { - fn from(__sv_bar: v1::Bar) -> Self { - Self { baz: __sv_bar.baz.into() } - } -} -#[automatically_derived] -impl ::std::convert::From for v1::Bar { - fn from(__sv_bar: v2alpha1::Bar) -> Self { - Self { baz: __sv_bar.baz.into() } - } -} -#[automatically_derived] -pub(crate) mod v2alpha1 { - use super::*; - pub struct Foo { - pub bar: usize, - pub baz: bool, - #[deprecated] - pub deprecated_foo: String, - } - pub struct Bar { - pub baz: String, - } -} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@module_preserve.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@module_preserve.rs.snap deleted file mode 100644 index f4113c479..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@module_preserve.rs.snap +++ /dev/null @@ -1,98 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/module_preserve.rs ---- -#[automatically_derived] -pub(crate) mod versioned { - pub mod v1alpha1 { - use super::*; - pub struct Foo { - pub bar: usize, - pub foo: String, - } - pub struct Bar { - pub baz: String, - } - } - impl ::std::convert::From for v1::Foo { - fn from(__sv_foo: v1alpha1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - baz: ::std::default::Default::default(), - foo: __sv_foo.foo.into(), - } - } - } - impl ::std::convert::From for v1alpha1::Foo { - fn from(__sv_foo: v1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - foo: __sv_foo.foo.into(), - } - } - } - impl ::std::convert::From for v1::Bar { - fn from(__sv_bar: v1alpha1::Bar) -> Self { - Self { baz: __sv_bar.baz.into() } - } - } - impl ::std::convert::From for v1alpha1::Bar { - fn from(__sv_bar: v1::Bar) -> Self { - Self { baz: __sv_bar.baz.into() } - } - } - pub mod v1 { - use super::*; - pub struct Foo { - pub bar: usize, - pub baz: bool, - pub foo: String, - } - pub struct Bar { - pub baz: String, - } - } - #[allow(deprecated)] - impl ::std::convert::From for v2alpha1::Foo { - fn from(__sv_foo: v1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - baz: __sv_foo.baz.into(), - deprecated_foo: __sv_foo.foo.into(), - } - } - } - #[allow(deprecated)] - impl ::std::convert::From for v1::Foo { - fn from(__sv_foo: v2alpha1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - baz: __sv_foo.baz.into(), - foo: __sv_foo.deprecated_foo.into(), - } - } - } - impl ::std::convert::From for v2alpha1::Bar { - fn from(__sv_bar: v1::Bar) -> Self { - Self { baz: __sv_bar.baz.into() } - } - } - impl ::std::convert::From for v1::Bar { - fn from(__sv_bar: v2alpha1::Bar) -> Self { - Self { baz: __sv_bar.baz.into() } - } - } - pub mod v2alpha1 { - use super::*; - pub struct Foo { - pub bar: usize, - pub baz: bool, - #[deprecated] - pub deprecated_foo: String, - } - pub struct Bar { - pub baz: String, - } - } -} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@skip_from_for_version.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@skip_from_for_version.rs.snap deleted file mode 100644 index 34f9a8db4..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@skip_from_for_version.rs.snap +++ /dev/null @@ -1,44 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/skip_from_for_version.rs ---- -#[automatically_derived] -pub mod v1alpha1 { - use super::*; - pub struct Foo { - pub baz: bool, - } -} -#[automatically_derived] -impl ::std::convert::From for v1beta1::Foo { - fn from(__sv_foo: v1alpha1::Foo) -> Self { - Self { - bar: ::std::default::Default::default(), - baz: __sv_foo.baz.into(), - } - } -} -#[automatically_derived] -impl ::std::convert::From for v1alpha1::Foo { - fn from(__sv_foo: v1beta1::Foo) -> Self { - Self { baz: __sv_foo.baz.into() } - } -} -#[automatically_derived] -pub mod v1beta1 { - use super::*; - pub struct Foo { - pub bar: usize, - pub baz: bool, - } -} -#[automatically_derived] -pub mod v1 { - use super::*; - pub struct Foo { - #[deprecated = "not needed"] - pub deprecated_bar: usize, - pub baz: bool, - } -} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@skip_from_module.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@skip_from_module.rs.snap deleted file mode 100644 index 75dc220f9..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@skip_from_module.rs.snap +++ /dev/null @@ -1,20 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/skip_from_module.rs ---- -#[automatically_derived] -pub(crate) mod v1alpha1 { - use super::*; - pub struct Foo { - pub bar: usize, - } -} -#[automatically_derived] -pub(crate) mod v1beta1 { - use super::*; - pub struct Foo { - pub bar: usize, - pub baz: bool, - } -} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@skip_from_module_for_version.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@skip_from_module_for_version.rs.snap deleted file mode 100644 index 578388d05..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@skip_from_module_for_version.rs.snap +++ /dev/null @@ -1,69 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/skip_from_module_for_version.rs ---- -#[automatically_derived] -pub(crate) mod v1alpha1 { - use super::*; - pub struct Foo { - pub bar: usize, - } - pub struct Bar { - pub foo: usize, - } -} -#[automatically_derived] -impl ::std::convert::From for v1beta1::Foo { - fn from(__sv_foo: v1alpha1::Foo) -> Self { - Self { - bar: __sv_foo.bar.into(), - baz: ::std::default::Default::default(), - } - } -} -#[automatically_derived] -impl ::std::convert::From for v1alpha1::Foo { - fn from(__sv_foo: v1beta1::Foo) -> Self { - Self { bar: __sv_foo.bar.into() } - } -} -#[automatically_derived] -impl ::std::convert::From for v1beta1::Bar { - fn from(__sv_bar: v1alpha1::Bar) -> Self { - Self { - foo: __sv_bar.foo.into(), - faz: ::std::default::Default::default(), - } - } -} -#[automatically_derived] -impl ::std::convert::From for v1alpha1::Bar { - fn from(__sv_bar: v1beta1::Bar) -> Self { - Self { foo: __sv_bar.foo.into() } - } -} -#[automatically_derived] -pub(crate) mod v1beta1 { - use super::*; - pub struct Foo { - pub bar: usize, - pub baz: bool, - } - pub struct Bar { - pub foo: usize, - pub faz: bool, - } -} -#[automatically_derived] -pub(crate) mod v1 { - use super::*; - pub struct Foo { - pub bar: usize, - pub baz: bool, - } - pub struct Bar { - pub foo: usize, - pub faz: bool, - } -} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@added.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@added.rs.snap similarity index 82% rename from crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@added.rs.snap rename to crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@added.rs.snap index 95a89142a..bc72d1dfc 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@added.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@added.rs.snap @@ -1,7 +1,7 @@ --- source: crates/stackable-versioned-macros/src/lib.rs expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/added.rs +input_file: crates/stackable-versioned-macros/tests/inputs/pass/added.rs --- #[automatically_derived] mod v1alpha1 { @@ -11,7 +11,7 @@ mod v1alpha1 { } } #[automatically_derived] -impl ::std::convert::From for v1alpha2::Foo { +impl ::core::convert::From for v1alpha2::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { Self { username: __sv_foo.username.into(), @@ -20,7 +20,7 @@ impl ::std::convert::From for v1alpha2::Foo { } } #[automatically_derived] -impl ::std::convert::From for v1alpha1::Foo { +impl ::core::convert::From for v1alpha1::Foo { fn from(__sv_foo: v1alpha2::Foo) -> Self { Self { username: __sv_foo.username.into(), @@ -36,7 +36,7 @@ mod v1alpha2 { } } #[automatically_derived] -impl ::std::convert::From for v1beta1::Foo { +impl ::core::convert::From for v1beta1::Foo { fn from(__sv_foo: v1alpha2::Foo) -> Self { Self { username: __sv_foo.username.into(), @@ -46,7 +46,7 @@ impl ::std::convert::From for v1beta1::Foo { } } #[automatically_derived] -impl ::std::convert::From for v1alpha2::Foo { +impl ::core::convert::From for v1alpha2::Foo { fn from(__sv_foo: v1beta1::Foo) -> Self { Self { username: __sv_foo.username.into(), @@ -64,7 +64,7 @@ mod v1beta1 { } } #[automatically_derived] -impl ::std::convert::From for v1::Foo { +impl ::core::convert::From for v1::Foo { fn from(__sv_foo: v1beta1::Foo) -> Self { Self { username: __sv_foo.username.into(), @@ -74,7 +74,7 @@ impl ::std::convert::From for v1::Foo { } } #[automatically_derived] -impl ::std::convert::From for v1beta1::Foo { +impl ::core::convert::From for v1beta1::Foo { fn from(__sv_foo: v1::Foo) -> Self { Self { username: __sv_foo.username.into(), diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@basic.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@basic.rs.snap similarity index 82% rename from crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@basic.rs.snap rename to crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@basic.rs.snap index cad771f3f..3f981cccd 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@basic.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@basic.rs.snap @@ -1,7 +1,7 @@ --- source: crates/stackable-versioned-macros/src/lib.rs expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/k8s/pass/basic.rs +input_file: crates/stackable-versioned-macros/tests/inputs/pass/basic.rs --- #[automatically_derived] pub(crate) mod v1alpha1 { @@ -28,7 +28,27 @@ pub(crate) mod v1alpha1 { } } #[automatically_derived] -impl ::std::convert::From for v1beta1::FooSpec { +impl ::core::convert::From for v1beta1::Foo { + fn from(__sv_foo: v1alpha1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + status: __sv_foo.status, + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1alpha1::Foo { + fn from(__sv_foo: v1beta1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + status: __sv_foo.status, + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1beta1::FooSpec { fn from(__sv_foospec: v1alpha1::FooSpec) -> Self { Self { bah: ::std::default::Default::default(), @@ -37,7 +57,7 @@ impl ::std::convert::From for v1beta1::FooSpec { } } #[automatically_derived] -impl ::std::convert::From for v1alpha1::FooSpec { +impl ::core::convert::From for v1alpha1::FooSpec { fn from(__sv_foospec: v1beta1::FooSpec) -> Self { Self { baz: __sv_foospec.baz.into(), @@ -70,7 +90,27 @@ pub(crate) mod v1beta1 { } } #[automatically_derived] -impl ::std::convert::From for v1::FooSpec { +impl ::core::convert::From for v1::Foo { + fn from(__sv_foo: v1beta1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + status: __sv_foo.status, + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1beta1::Foo { + fn from(__sv_foo: v1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + status: __sv_foo.status, + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1::FooSpec { fn from(__sv_foospec: v1beta1::FooSpec) -> Self { Self { bar: __sv_foospec.bah.into(), @@ -79,7 +119,7 @@ impl ::std::convert::From for v1::FooSpec { } } #[automatically_derived] -impl ::std::convert::From for v1beta1::FooSpec { +impl ::core::convert::From for v1beta1::FooSpec { fn from(__sv_foospec: v1::FooSpec) -> Self { Self { bah: usize_to_u16(__sv_foospec.bar), @@ -113,6 +153,7 @@ pub(crate) mod v1 { } } #[automatically_derived] +#[derive(::core::fmt::Debug)] pub(crate) enum Foo { V1Alpha1(v1alpha1::Foo), V1Beta1(v1beta1::Foo), @@ -211,12 +252,8 @@ impl Foo { })?; match (current_object, desired_api_version) { (Self::V1Alpha1(__sv_foo), FooVersion::V1Beta1) => { - let converted: v1beta1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1Beta1(v1beta1::Foo { - metadata: __sv_foo.metadata, - status: __sv_foo.status, - spec: converted, - }); + let converted: v1beta1::Foo = __sv_foo.into(); + let desired_object = Self::V1Beta1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -225,13 +262,9 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1Alpha1(__sv_foo), FooVersion::V1) => { - let converted: v1beta1::FooSpec = __sv_foo.spec.into(); - let converted: v1::FooSpec = converted.into(); - let desired_object = Self::V1(v1::Foo { - metadata: __sv_foo.metadata, - status: __sv_foo.status, - spec: converted, - }); + let converted: v1beta1::Foo = __sv_foo.into(); + let converted: v1::Foo = converted.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -240,12 +273,8 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1Beta1(__sv_foo), FooVersion::V1Alpha1) => { - let converted: v1alpha1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1Alpha1(v1alpha1::Foo { - metadata: __sv_foo.metadata, - status: __sv_foo.status, - spec: converted, - }); + let converted: v1alpha1::Foo = __sv_foo.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -254,12 +283,8 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1Beta1(__sv_foo), FooVersion::V1) => { - let converted: v1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1(v1::Foo { - metadata: __sv_foo.metadata, - status: __sv_foo.status, - spec: converted, - }); + let converted: v1::Foo = __sv_foo.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -268,13 +293,9 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1(__sv_foo), FooVersion::V1Alpha1) => { - let converted: v1beta1::FooSpec = __sv_foo.spec.into(); - let converted: v1alpha1::FooSpec = converted.into(); - let desired_object = Self::V1Alpha1(v1alpha1::Foo { - metadata: __sv_foo.metadata, - status: __sv_foo.status, - spec: converted, - }); + let converted: v1beta1::Foo = __sv_foo.into(); + let converted: v1alpha1::Foo = converted.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -283,12 +304,8 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1(__sv_foo), FooVersion::V1Beta1) => { - let converted: v1beta1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1Beta1(v1beta1::Foo { - metadata: __sv_foo.metadata, - status: __sv_foo.status, - spec: converted, - }); + let converted: v1beta1::Foo = __sv_foo.into(); + let desired_object = Self::V1Beta1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -304,24 +321,24 @@ impl Foo { fn from_json_object( object_value: ::serde_json::Value, ) -> ::std::result::Result { - let object_kind = object_value + let kind = object_value .get("kind") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "kind".to_owned(), })? .as_str() .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { field: "kind".to_owned(), })?; - if object_kind != "Foo" { + if kind != "Foo" { return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { - kind: object_kind.to_owned(), + kind: kind.to_owned(), expected: "Foo".to_owned(), }); } let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "apiVersion".to_owned(), })? .as_str() @@ -369,17 +386,17 @@ impl Foo { } } #[automatically_derived] -#[derive(Copy, Clone, Debug)] +#[derive(::core::marker::Copy, ::core::clone::Clone, ::core::fmt::Debug)] pub(crate) enum FooVersion { V1Alpha1, V1Beta1, V1, } #[automatically_derived] -impl ::std::fmt::Display for FooVersion { +impl ::core::fmt::Display for FooVersion { fn fmt( &self, - f: &mut ::std::fmt::Formatter<'_>, + f: &mut ::core::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { f.write_str(self.as_version_str()) } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@conversion_tracking.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@conversion_tracking.rs.snap similarity index 68% rename from crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@conversion_tracking.rs.snap rename to crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@conversion_tracking.rs.snap index 9a7ee26d5..353430d84 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@conversion_tracking.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@conversion_tracking.rs.snap @@ -1,7 +1,7 @@ --- source: crates/stackable-versioned-macros/src/lib.rs expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/k8s/pass/conversion_tracking.rs +input_file: crates/stackable-versioned-macros/tests/inputs/pass/conversion_tracking.rs --- #[automatically_derived] pub(crate) mod v1alpha1 { @@ -18,20 +18,90 @@ pub(crate) mod v1alpha1 { } } #[automatically_derived] -impl ::std::convert::From for v1beta1::FooSpec { - fn from(__sv_foospec: v1alpha1::FooSpec) -> Self { +impl ::core::convert::From for v1beta1::Foo { + fn from(__sv_foo: v1alpha1::Foo) -> Self { + let mut status = __sv_foo.status.unwrap_or_default(); + let spec = >::tracking_from(__sv_foo.spec, &mut status, ""); Self { - bah: ::std::default::Default::default(), - baz: __sv_foospec.baz.into(), + metadata: __sv_foo.metadata, + status: Some(status), + spec, } } } #[automatically_derived] -impl ::std::convert::From for v1alpha1::FooSpec { - fn from(__sv_foospec: v1beta1::FooSpec) -> Self { +impl ::core::convert::From for v1alpha1::Foo { + fn from(__sv_foo: v1beta1::Foo) -> Self { + let mut status = __sv_foo.status.unwrap_or_default(); + let spec = >::tracking_from(__sv_foo.spec, &mut status, ""); Self { + metadata: __sv_foo.metadata, + status: Some(status), + spec, + } + } +} +#[automatically_derived] +impl ::stackable_versioned::TrackingFrom for v1beta1::FooSpec +where + S: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from( + __sv_foospec: v1alpha1::FooSpec, + status: &mut S, + parent: &str, + ) -> Self { + use ::stackable_versioned::TrackingInto as _; + let __sv_bah_path = ::stackable_versioned::jthong_path(parent, "bah"); + let mut spec = Self { + bah: ::std::default::Default::default(), baz: __sv_foospec.baz.into(), + }; + if let Some(upgrades) = status.changes().upgrades.remove(&"v1beta1".to_owned()) { + for ::stackable_versioned::ChangedValue { field_name, value } in upgrades { + match field_name { + field_name if field_name == __sv_bah_path => { + spec.bah = serde_yaml::from_value(value).unwrap(); + } + _ => unreachable!(), + } + } } + spec + } +} +#[automatically_derived] +impl ::stackable_versioned::TrackingFrom for v1alpha1::FooSpec +where + S: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from( + __sv_foospec: v1beta1::FooSpec, + status: &mut S, + parent: &str, + ) -> Self { + use ::stackable_versioned::TrackingInto as _; + let __sv_bah_path = ::stackable_versioned::jthong_path(parent, "bah"); + let upgrades = status + .changes() + .upgrades + .entry("v1beta1".to_owned()) + .or_default(); + upgrades + .push(::stackable_versioned::ChangedValue { + field_name: __sv_bah_path, + value: ::serde_yaml::to_value(&__sv_foospec.bah).unwrap(), + }); + let mut spec = Self { + baz: __sv_foospec.baz.into(), + }; + spec } } #[automatically_derived] @@ -50,21 +120,65 @@ pub(crate) mod v1beta1 { } } #[automatically_derived] -impl ::std::convert::From for v1::FooSpec { - fn from(__sv_foospec: v1beta1::FooSpec) -> Self { +impl ::core::convert::From for v1::Foo { + fn from(__sv_foo: v1beta1::Foo) -> Self { + let mut status = __sv_foo.status.unwrap_or_default(); + let spec = >::tracking_from(__sv_foo.spec, &mut status, ""); Self { - bar: __sv_foospec.bah.into(), - baz: __sv_foospec.baz.into(), + metadata: __sv_foo.metadata, + status: Some(status), + spec, } } } #[automatically_derived] -impl ::std::convert::From for v1beta1::FooSpec { - fn from(__sv_foospec: v1::FooSpec) -> Self { +impl ::core::convert::From for v1beta1::Foo { + fn from(__sv_foo: v1::Foo) -> Self { + let mut status = __sv_foo.status.unwrap_or_default(); + let spec = >::tracking_from(__sv_foo.spec, &mut status, ""); Self { + metadata: __sv_foo.metadata, + status: Some(status), + spec, + } + } +} +#[automatically_derived] +impl ::stackable_versioned::TrackingFrom for v1::FooSpec +where + S: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from( + __sv_foospec: v1beta1::FooSpec, + status: &mut S, + parent: &str, + ) -> Self { + use ::stackable_versioned::TrackingInto as _; + let mut spec = Self { + bar: __sv_foospec.bah.into(), + baz: __sv_foospec.baz.into(), + }; + spec + } +} +#[automatically_derived] +impl ::stackable_versioned::TrackingFrom for v1beta1::FooSpec +where + S: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from(__sv_foospec: v1::FooSpec, status: &mut S, parent: &str) -> Self { + use ::stackable_versioned::TrackingInto as _; + let mut spec = Self { bah: __sv_foospec.bar.into(), baz: __sv_foospec.baz.into(), - } + }; + spec } } #[automatically_derived] @@ -83,6 +197,7 @@ pub(crate) mod v1 { } } #[automatically_derived] +#[derive(::core::fmt::Debug)] pub(crate) enum Foo { V1Alpha1(v1alpha1::Foo), V1Beta1(v1beta1::Foo), @@ -181,12 +296,8 @@ impl Foo { })?; match (current_object, desired_api_version) { (Self::V1Alpha1(__sv_foo), FooVersion::V1Beta1) => { - let converted: v1beta1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1Beta1(v1beta1::Foo { - metadata: __sv_foo.metadata, - status: __sv_foo.status, - spec: converted, - }); + let converted: v1beta1::Foo = __sv_foo.into(); + let desired_object = Self::V1Beta1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -195,13 +306,9 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1Alpha1(__sv_foo), FooVersion::V1) => { - let converted: v1beta1::FooSpec = __sv_foo.spec.into(); - let converted: v1::FooSpec = converted.into(); - let desired_object = Self::V1(v1::Foo { - metadata: __sv_foo.metadata, - status: __sv_foo.status, - spec: converted, - }); + let converted: v1beta1::Foo = __sv_foo.into(); + let converted: v1::Foo = converted.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -210,12 +317,8 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1Beta1(__sv_foo), FooVersion::V1Alpha1) => { - let converted: v1alpha1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1Alpha1(v1alpha1::Foo { - metadata: __sv_foo.metadata, - status: __sv_foo.status, - spec: converted, - }); + let converted: v1alpha1::Foo = __sv_foo.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -224,12 +327,8 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1Beta1(__sv_foo), FooVersion::V1) => { - let converted: v1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1(v1::Foo { - metadata: __sv_foo.metadata, - status: __sv_foo.status, - spec: converted, - }); + let converted: v1::Foo = __sv_foo.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -238,13 +337,9 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1(__sv_foo), FooVersion::V1Alpha1) => { - let converted: v1beta1::FooSpec = __sv_foo.spec.into(); - let converted: v1alpha1::FooSpec = converted.into(); - let desired_object = Self::V1Alpha1(v1alpha1::Foo { - metadata: __sv_foo.metadata, - status: __sv_foo.status, - spec: converted, - }); + let converted: v1beta1::Foo = __sv_foo.into(); + let converted: v1alpha1::Foo = converted.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -253,12 +348,8 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1(__sv_foo), FooVersion::V1Beta1) => { - let converted: v1beta1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1Beta1(v1beta1::Foo { - metadata: __sv_foo.metadata, - status: __sv_foo.status, - spec: converted, - }); + let converted: v1beta1::Foo = __sv_foo.into(); + let desired_object = Self::V1Beta1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -274,24 +365,24 @@ impl Foo { fn from_json_object( object_value: ::serde_json::Value, ) -> ::std::result::Result { - let object_kind = object_value + let kind = object_value .get("kind") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "kind".to_owned(), })? .as_str() .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { field: "kind".to_owned(), })?; - if object_kind != "Foo" { + if kind != "Foo" { return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { - kind: object_kind.to_owned(), + kind: kind.to_owned(), expected: "Foo".to_owned(), }); } let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "apiVersion".to_owned(), })? .as_str() @@ -339,17 +430,17 @@ impl Foo { } } #[automatically_derived] -#[derive(Copy, Clone, Debug)] +#[derive(::core::marker::Copy, ::core::clone::Clone, ::core::fmt::Debug)] pub(crate) enum FooVersion { V1Alpha1, V1Beta1, V1, } #[automatically_derived] -impl ::std::fmt::Display for FooVersion { +impl ::core::fmt::Display for FooVersion { fn fmt( &self, - f: &mut ::std::fmt::Formatter<'_>, + f: &mut ::core::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { f.write_str(self.as_version_str()) } @@ -388,6 +479,7 @@ impl FooVersion { #[automatically_derived] #[derive( ::core::clone::Clone, + ::core::default::Default, ::core::fmt::Debug, ::serde::Deserialize, ::serde::Serialize, @@ -399,3 +491,9 @@ pub struct FooStatusWithChangedValues { #[serde(flatten)] pub status: FooStatus, } +#[automatically_derived] +impl ::stackable_versioned::TrackingStatus for FooStatusWithChangedValues { + fn changes(&mut self) -> &mut ::stackable_versioned::ChangedValues { + &mut self.changed_values + } +} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@crate_overrides.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@crate_overrides.rs.snap similarity index 83% rename from crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@crate_overrides.rs.snap rename to crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@crate_overrides.rs.snap index 2a120aa0e..495389a6d 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@crate_overrides.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@crate_overrides.rs.snap @@ -1,7 +1,7 @@ --- source: crates/stackable-versioned-macros/src/lib.rs expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/k8s/pass/crate_overrides.rs +input_file: crates/stackable-versioned-macros/tests/inputs/pass/crate_overrides.rs --- #[automatically_derived] pub mod v1alpha1 { @@ -25,7 +25,25 @@ pub mod v1alpha1 { } } #[automatically_derived] -impl ::std::convert::From for v1beta1::FooSpec { +impl ::core::convert::From for v1beta1::Foo { + fn from(__sv_foo: v1alpha1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1alpha1::Foo { + fn from(__sv_foo: v1beta1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1beta1::FooSpec { fn from(__sv_foospec: v1alpha1::FooSpec) -> Self { Self { bah: ::std::default::Default::default(), @@ -34,7 +52,7 @@ impl ::std::convert::From for v1beta1::FooSpec { } } #[automatically_derived] -impl ::std::convert::From for v1alpha1::FooSpec { +impl ::core::convert::From for v1alpha1::FooSpec { fn from(__sv_foospec: v1beta1::FooSpec) -> Self { Self { baz: __sv_foospec.baz.into(), @@ -64,7 +82,25 @@ pub mod v1beta1 { } } #[automatically_derived] -impl ::std::convert::From for v1::FooSpec { +impl ::core::convert::From for v1::Foo { + fn from(__sv_foo: v1beta1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1beta1::Foo { + fn from(__sv_foo: v1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1::FooSpec { fn from(__sv_foospec: v1beta1::FooSpec) -> Self { Self { bar: __sv_foospec.bah.into(), @@ -73,7 +109,7 @@ impl ::std::convert::From for v1::FooSpec { } } #[automatically_derived] -impl ::std::convert::From for v1beta1::FooSpec { +impl ::core::convert::From for v1beta1::FooSpec { fn from(__sv_foospec: v1::FooSpec) -> Self { Self { bah: __sv_foospec.bar.into(), @@ -104,6 +140,7 @@ pub mod v1 { } } #[automatically_derived] +#[derive(::core::fmt::Debug)] pub enum Foo { V1Alpha1(v1alpha1::Foo), V1Beta1(v1beta1::Foo), @@ -202,11 +239,8 @@ impl Foo { })?; match (current_object, desired_api_version) { (Self::V1Alpha1(__sv_foo), FooVersion::V1Beta1) => { - let converted: v1beta1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1Beta1(v1beta1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1beta1::Foo = __sv_foo.into(); + let desired_object = Self::V1Beta1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -215,12 +249,9 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1Alpha1(__sv_foo), FooVersion::V1) => { - let converted: v1beta1::FooSpec = __sv_foo.spec.into(); - let converted: v1::FooSpec = converted.into(); - let desired_object = Self::V1(v1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1beta1::Foo = __sv_foo.into(); + let converted: v1::Foo = converted.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -229,11 +260,8 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1Beta1(__sv_foo), FooVersion::V1Alpha1) => { - let converted: v1alpha1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1Alpha1(v1alpha1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1alpha1::Foo = __sv_foo.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -242,11 +270,8 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1Beta1(__sv_foo), FooVersion::V1) => { - let converted: v1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1(v1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1::Foo = __sv_foo.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -255,12 +280,9 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1(__sv_foo), FooVersion::V1Alpha1) => { - let converted: v1beta1::FooSpec = __sv_foo.spec.into(); - let converted: v1alpha1::FooSpec = converted.into(); - let desired_object = Self::V1Alpha1(v1alpha1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1beta1::Foo = __sv_foo.into(); + let converted: v1alpha1::Foo = converted.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -269,11 +291,8 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1(__sv_foo), FooVersion::V1Beta1) => { - let converted: v1beta1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1Beta1(v1beta1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1beta1::Foo = __sv_foo.into(); + let desired_object = Self::V1Beta1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -289,24 +308,24 @@ impl Foo { fn from_json_object( object_value: ::serde_json::Value, ) -> ::std::result::Result { - let object_kind = object_value + let kind = object_value .get("kind") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "kind".to_owned(), })? .as_str() .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { field: "kind".to_owned(), })?; - if object_kind != "Foo" { + if kind != "Foo" { return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { - kind: object_kind.to_owned(), + kind: kind.to_owned(), expected: "Foo".to_owned(), }); } let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "apiVersion".to_owned(), })? .as_str() @@ -354,17 +373,17 @@ impl Foo { } } #[automatically_derived] -#[derive(Copy, Clone, Debug)] +#[derive(::core::marker::Copy, ::core::clone::Clone, ::core::fmt::Debug)] pub enum FooVersion { V1Alpha1, V1Beta1, V1, } #[automatically_derived] -impl ::std::fmt::Display for FooVersion { +impl ::core::fmt::Display for FooVersion { fn fmt( &self, - f: &mut ::std::fmt::Formatter<'_>, + f: &mut ::core::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { f.write_str(self.as_version_str()) } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@docs.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@docs.rs.snap new file mode 100644 index 000000000..a673fe157 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@docs.rs.snap @@ -0,0 +1,247 @@ +--- +source: crates/stackable-versioned-macros/src/lib.rs +expression: formatted +input_file: crates/stackable-versioned-macros/tests/inputs/pass/docs.rs +--- +#[automatically_derived] +mod v1alpha1 { + use super::*; + /// Test + #[derive(Default)] + pub struct Foo { + /// This field is available in every version (so far). + pub foo: String, + /// Keep the main field docs the same, even after the field is deprecated. + pub bar: String, + /// This is will keep changing over time. + pub qoox: String, + } +} +#[automatically_derived] +#[allow(deprecated)] +impl ::stackable_versioned::TrackingFrom for v1beta1::Foo +where + S: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from(__sv_foo: v1alpha1::Foo, status: &mut S, parent: &str) -> Self { + use ::stackable_versioned::TrackingInto as _; + let __sv_baz_path = ::stackable_versioned::jthong_path(parent, "baz"); + let mut spec = Self { + foo: __sv_foo.foo.into(), + deprecated_bar: __sv_foo.bar.into(), + baz: ::std::default::Default::default(), + qaax: __sv_foo.qoox.into(), + }; + if let Some(upgrades) = status.changes().upgrades.remove(&"v1beta1".to_owned()) { + for ::stackable_versioned::ChangedValue { field_name, value } in upgrades { + match field_name { + field_name if field_name == __sv_baz_path => { + spec.baz = serde_yaml::from_value(value).unwrap(); + } + _ => unreachable!(), + } + } + } + spec + } +} +#[automatically_derived] +#[allow(deprecated)] +impl ::stackable_versioned::TrackingFrom for v1alpha1::Foo +where + S: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from(__sv_foo: v1beta1::Foo, status: &mut S, parent: &str) -> Self { + use ::stackable_versioned::TrackingInto as _; + let __sv_baz_path = ::stackable_versioned::jthong_path(parent, "baz"); + let upgrades = status + .changes() + .upgrades + .entry("v1beta1".to_owned()) + .or_default(); + upgrades + .push(::stackable_versioned::ChangedValue { + field_name: __sv_baz_path, + value: ::serde_yaml::to_value(&__sv_foo.baz).unwrap(), + }); + let mut spec = Self { + foo: __sv_foo.foo.into(), + bar: __sv_foo.deprecated_bar.into(), + qoox: __sv_foo.qaax.into(), + }; + spec + } +} +#[automatically_derived] +mod v1beta1 { + use super::*; + ///Additional docs for this version which are purposefully long to + ///show how manual line wrapping works. \ + ///Multi-line docs are also supported, as per regular doc-comments. + /// Test + #[derive(Default)] + pub struct Foo { + /// This field is available in every version (so far). + pub foo: String, + /// Keep the main field docs the same, even after the field is deprecated. + #[deprecated = "gone"] + pub deprecated_bar: String, + /// This is for baz + pub baz: String, + /// This is will keep changing over time. + pub qaax: String, + } +} +#[automatically_derived] +#[allow(deprecated)] +impl ::stackable_versioned::TrackingFrom for v1beta2::Foo +where + S: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from(__sv_foo: v1beta1::Foo, status: &mut S, parent: &str) -> Self { + use ::stackable_versioned::TrackingInto as _; + let mut spec = Self { + foo: __sv_foo.foo.into(), + deprecated_bar: __sv_foo.deprecated_bar.into(), + baz: __sv_foo.baz.into(), + qaax: __sv_foo.qaax.into(), + }; + spec + } +} +#[automatically_derived] +#[allow(deprecated)] +impl ::stackable_versioned::TrackingFrom for v1beta1::Foo +where + S: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from(__sv_foo: v1beta2::Foo, status: &mut S, parent: &str) -> Self { + use ::stackable_versioned::TrackingInto as _; + let mut spec = Self { + foo: __sv_foo.foo.into(), + deprecated_bar: __sv_foo.deprecated_bar.into(), + baz: __sv_foo.baz.into(), + qaax: __sv_foo.qaax.into(), + }; + spec + } +} +#[automatically_derived] +mod v1beta2 { + use super::*; + /// Test + #[derive(Default)] + pub struct Foo { + /// This field is available in every version (so far). + pub foo: String, + /// Keep the main field docs the same, even after the field is deprecated. + #[deprecated] + pub deprecated_bar: String, + /// This is for baz + pub baz: String, + /// This is will keep changing over time. + pub qaax: String, + } +} +#[automatically_derived] +#[allow(deprecated)] +impl ::stackable_versioned::TrackingFrom for v1::Foo +where + S: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from(__sv_foo: v1beta2::Foo, status: &mut S, parent: &str) -> Self { + use ::stackable_versioned::TrackingInto as _; + let mut spec = Self { + foo: __sv_foo.foo.into(), + deprecated_bar: __sv_foo.deprecated_bar.into(), + baz: __sv_foo.baz.into(), + quux: __sv_foo.qaax.into(), + }; + spec + } +} +#[automatically_derived] +#[allow(deprecated)] +impl ::stackable_versioned::TrackingFrom for v1beta2::Foo +where + S: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from(__sv_foo: v1::Foo, status: &mut S, parent: &str) -> Self { + use ::stackable_versioned::TrackingInto as _; + let mut spec = Self { + foo: __sv_foo.foo.into(), + deprecated_bar: __sv_foo.deprecated_bar.into(), + baz: __sv_foo.baz.into(), + qaax: __sv_foo.quux.into(), + }; + spec + } +} +#[automatically_derived] +mod v1 { + use super::*; + /// Test + #[derive(Default)] + pub struct Foo { + /// This field is available in every version (so far). + pub foo: String, + /// Keep the main field docs the same, even after the field is deprecated. + #[deprecated] + pub deprecated_bar: String, + /// This is for baz + pub baz: String, + /// This is will keep changing over time. + pub quux: String, + } +} +#[automatically_derived] +#[allow(deprecated)] +impl ::stackable_versioned::TrackingFrom for v2::Foo +where + S: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from(__sv_foo: v1::Foo, status: &mut S, parent: &str) -> Self { + use ::stackable_versioned::TrackingInto as _; + let mut spec = Self { + foo: __sv_foo.foo.into(), + deprecated_bar: __sv_foo.deprecated_bar.into(), + baz: __sv_foo.baz.into(), + quux: __sv_foo.quux.into(), + }; + spec + } +} +#[automatically_derived] +#[allow(deprecated)] +impl ::stackable_versioned::TrackingFrom for v1::Foo +where + S: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from(__sv_foo: v2::Foo, status: &mut S, parent: &str) -> Self { + use ::stackable_versioned::TrackingInto as _; + let mut spec = Self { + foo: __sv_foo.foo.into(), + deprecated_bar: __sv_foo.deprecated_bar.into(), + baz: __sv_foo.baz.into(), + quux: __sv_foo.quux.into(), + }; + spec + } +} +#[automatically_derived] +mod v2 { + use super::*; + /// Test + #[derive(Default)] + pub struct Foo { + /// This field is available in every version (so far). + pub foo: String, + /// Keep the main field docs the same, even after the field is deprecated. + #[deprecated] + pub deprecated_bar: String, + /// This is for baz + pub baz: String, + /// This is will keep changing over time. + pub quux: String, + } +} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@downgrade_with.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@downgrade_with.rs.snap similarity index 75% rename from crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@downgrade_with.rs.snap rename to crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@downgrade_with.rs.snap index 28915bc31..b55353b96 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@downgrade_with.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@downgrade_with.rs.snap @@ -1,7 +1,7 @@ --- source: crates/stackable-versioned-macros/src/lib.rs expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/downgrade_with.rs +input_file: crates/stackable-versioned-macros/tests/inputs/pass/downgrade_with.rs --- #[automatically_derived] mod v1alpha1 { @@ -11,13 +11,13 @@ mod v1alpha1 { } } #[automatically_derived] -impl ::std::convert::From for v1::Foo { +impl ::core::convert::From for v1::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { Self { baz: __sv_foo.bar.into() } } } #[automatically_derived] -impl ::std::convert::From for v1alpha1::Foo { +impl ::core::convert::From for v1alpha1::Foo { fn from(__sv_foo: v1::Foo) -> Self { Self { bar: u32_to_u16(__sv_foo.baz), @@ -32,13 +32,13 @@ mod v1 { } } #[automatically_derived] -impl ::std::convert::From for v2::Foo { +impl ::core::convert::From for v2::Foo { fn from(__sv_foo: v1::Foo) -> Self { Self { baz: __sv_foo.baz.into() } } } #[automatically_derived] -impl ::std::convert::From for v1::Foo { +impl ::core::convert::From for v1::Foo { fn from(__sv_foo: v2::Foo) -> Self { Self { baz: u64_to_u32(__sv_foo.baz), diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@module.rs.snap similarity index 83% rename from crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module.rs.snap rename to crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@module.rs.snap index 0eec1bba4..21ec44f82 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@module.rs.snap @@ -1,7 +1,7 @@ --- source: crates/stackable-versioned-macros/src/lib.rs expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/k8s/pass/module.rs +input_file: crates/stackable-versioned-macros/tests/inputs/pass/module.rs --- #[automatically_derived] pub(crate) mod v1alpha1 { @@ -51,19 +51,37 @@ pub(crate) mod v1alpha1 { } } #[automatically_derived] -impl ::std::convert::From for v1::Baz { +impl ::core::convert::From for v1::Baz { fn from(__sv_baz: v1alpha1::Baz) -> Self { Self { boom: __sv_baz.boom.into() } } } #[automatically_derived] -impl ::std::convert::From for v1alpha1::Baz { +impl ::core::convert::From for v1alpha1::Baz { fn from(__sv_baz: v1::Baz) -> Self { Self { boom: __sv_baz.boom.into() } } } #[automatically_derived] -impl ::std::convert::From for v1::FooSpec { +impl ::core::convert::From for v1::Foo { + fn from(__sv_foo: v1alpha1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1alpha1::Foo { + fn from(__sv_foo: v1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1::FooSpec { fn from(__sv_foospec: v1alpha1::FooSpec) -> Self { Self { bar: __sv_foospec.bar.into(), @@ -73,7 +91,7 @@ impl ::std::convert::From for v1::FooSpec { } } #[automatically_derived] -impl ::std::convert::From for v1alpha1::FooSpec { +impl ::core::convert::From for v1alpha1::FooSpec { fn from(__sv_foospec: v1::FooSpec) -> Self { Self { bar: __sv_foospec.bar.into(), @@ -82,7 +100,25 @@ impl ::std::convert::From for v1alpha1::FooSpec { } } #[automatically_derived] -impl ::std::convert::From for v1::BarSpec { +impl ::core::convert::From for v1::Bar { + fn from(__sv_bar: v1alpha1::Bar) -> Self { + Self { + metadata: __sv_bar.metadata, + spec: __sv_bar.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1alpha1::Bar { + fn from(__sv_bar: v1::Bar) -> Self { + Self { + metadata: __sv_bar.metadata, + spec: __sv_bar.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1::BarSpec { fn from(__sv_barspec: v1alpha1::BarSpec) -> Self { Self { baz: __sv_barspec.baz.into(), @@ -90,7 +126,7 @@ impl ::std::convert::From for v1::BarSpec { } } #[automatically_derived] -impl ::std::convert::From for v1alpha1::BarSpec { +impl ::core::convert::From for v1alpha1::BarSpec { fn from(__sv_barspec: v1::BarSpec) -> Self { Self { baz: __sv_barspec.baz.into(), @@ -98,7 +134,7 @@ impl ::std::convert::From for v1alpha1::BarSpec { } } #[automatically_derived] -impl ::std::convert::From for v1::Boom { +impl ::core::convert::From for v1::Boom { fn from(__sv_boom: v1alpha1::Boom) -> Self { match __sv_boom { v1alpha1::Boom::Big => v1::Boom::Big, @@ -107,7 +143,7 @@ impl ::std::convert::From for v1::Boom { } } #[automatically_derived] -impl ::std::convert::From for v1alpha1::Boom { +impl ::core::convert::From for v1alpha1::Boom { fn from(__sv_boom: v1::Boom) -> Self { match __sv_boom { v1::Boom::Big => v1alpha1::Boom::Big, @@ -159,20 +195,38 @@ pub(crate) mod v1 { } } #[automatically_derived] -impl ::std::convert::From for v2alpha1::Baz { +impl ::core::convert::From for v2alpha1::Baz { fn from(__sv_baz: v1::Baz) -> Self { Self { boom: __sv_baz.boom.into() } } } #[automatically_derived] -impl ::std::convert::From for v1::Baz { +impl ::core::convert::From for v1::Baz { fn from(__sv_baz: v2alpha1::Baz) -> Self { Self { boom: __sv_baz.boom.into() } } } #[automatically_derived] +impl ::core::convert::From for v2alpha1::Foo { + fn from(__sv_foo: v1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1::Foo { + fn from(__sv_foo: v2alpha1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } +} +#[automatically_derived] #[allow(deprecated)] -impl ::std::convert::From for v2alpha1::FooSpec { +impl ::core::convert::From for v2alpha1::FooSpec { fn from(__sv_foospec: v1::FooSpec) -> Self { Self { bar: __sv_foospec.bar.into(), @@ -183,7 +237,7 @@ impl ::std::convert::From for v2alpha1::FooSpec { } #[automatically_derived] #[allow(deprecated)] -impl ::std::convert::From for v1::FooSpec { +impl ::core::convert::From for v1::FooSpec { fn from(__sv_foospec: v2alpha1::FooSpec) -> Self { Self { bar: __sv_foospec.bar.into(), @@ -193,7 +247,25 @@ impl ::std::convert::From for v1::FooSpec { } } #[automatically_derived] -impl ::std::convert::From for v2alpha1::BarSpec { +impl ::core::convert::From for v2alpha1::Bar { + fn from(__sv_bar: v1::Bar) -> Self { + Self { + metadata: __sv_bar.metadata, + spec: __sv_bar.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1::Bar { + fn from(__sv_bar: v2alpha1::Bar) -> Self { + Self { + metadata: __sv_bar.metadata, + spec: __sv_bar.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v2alpha1::BarSpec { fn from(__sv_barspec: v1::BarSpec) -> Self { Self { baz: __sv_barspec.baz.into(), @@ -201,7 +273,7 @@ impl ::std::convert::From for v2alpha1::BarSpec { } } #[automatically_derived] -impl ::std::convert::From for v1::BarSpec { +impl ::core::convert::From for v1::BarSpec { fn from(__sv_barspec: v2alpha1::BarSpec) -> Self { Self { baz: __sv_barspec.baz.into(), @@ -209,7 +281,7 @@ impl ::std::convert::From for v1::BarSpec { } } #[automatically_derived] -impl ::std::convert::From for v2alpha1::Boom { +impl ::core::convert::From for v2alpha1::Boom { fn from(__sv_boom: v1::Boom) -> Self { match __sv_boom { v1::Boom::Big => v2alpha1::Boom::Big, @@ -218,7 +290,7 @@ impl ::std::convert::From for v2alpha1::Boom { } } #[automatically_derived] -impl ::std::convert::From for v1::Boom { +impl ::core::convert::From for v1::Boom { fn from(__sv_boom: v2alpha1::Boom) -> Self { match __sv_boom { v2alpha1::Boom::Big => v1::Boom::Big, @@ -276,6 +348,7 @@ pub(crate) mod v2alpha1 { } } #[automatically_derived] +#[derive(::core::fmt::Debug)] pub(crate) enum Foo { V1Alpha1(v1alpha1::Foo), V1(v1::Foo), @@ -374,11 +447,8 @@ impl Foo { })?; match (current_object, desired_api_version) { (Self::V1Alpha1(__sv_foo), FooVersion::V1) => { - let converted: v1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1(v1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1::Foo = __sv_foo.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -387,12 +457,9 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1Alpha1(__sv_foo), FooVersion::V2Alpha1) => { - let converted: v1::FooSpec = __sv_foo.spec.into(); - let converted: v2alpha1::FooSpec = converted.into(); - let desired_object = Self::V2Alpha1(v2alpha1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1::Foo = __sv_foo.into(); + let converted: v2alpha1::Foo = converted.into(); + let desired_object = Self::V2Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -401,11 +468,8 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1(__sv_foo), FooVersion::V1Alpha1) => { - let converted: v1alpha1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1Alpha1(v1alpha1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1alpha1::Foo = __sv_foo.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -414,11 +478,8 @@ impl Foo { converted_objects.push(desired_object); } (Self::V1(__sv_foo), FooVersion::V2Alpha1) => { - let converted: v2alpha1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V2Alpha1(v2alpha1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v2alpha1::Foo = __sv_foo.into(); + let desired_object = Self::V2Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -427,12 +488,9 @@ impl Foo { converted_objects.push(desired_object); } (Self::V2Alpha1(__sv_foo), FooVersion::V1Alpha1) => { - let converted: v1::FooSpec = __sv_foo.spec.into(); - let converted: v1alpha1::FooSpec = converted.into(); - let desired_object = Self::V1Alpha1(v1alpha1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1::Foo = __sv_foo.into(); + let converted: v1alpha1::Foo = converted.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -441,11 +499,8 @@ impl Foo { converted_objects.push(desired_object); } (Self::V2Alpha1(__sv_foo), FooVersion::V1) => { - let converted: v1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1(v1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1::Foo = __sv_foo.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -461,24 +516,24 @@ impl Foo { fn from_json_object( object_value: ::serde_json::Value, ) -> ::std::result::Result { - let object_kind = object_value + let kind = object_value .get("kind") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "kind".to_owned(), })? .as_str() .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { field: "kind".to_owned(), })?; - if object_kind != "Foo" { + if kind != "Foo" { return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { - kind: object_kind.to_owned(), + kind: kind.to_owned(), expected: "Foo".to_owned(), }); } let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "apiVersion".to_owned(), })? .as_str() @@ -526,17 +581,17 @@ impl Foo { } } #[automatically_derived] -#[derive(Copy, Clone, Debug)] +#[derive(::core::marker::Copy, ::core::clone::Clone, ::core::fmt::Debug)] pub(crate) enum FooVersion { V1Alpha1, V1, V2Alpha1, } #[automatically_derived] -impl ::std::fmt::Display for FooVersion { +impl ::core::fmt::Display for FooVersion { fn fmt( &self, - f: &mut ::std::fmt::Formatter<'_>, + f: &mut ::core::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { f.write_str(self.as_version_str()) } @@ -573,6 +628,7 @@ impl FooVersion { } } #[automatically_derived] +#[derive(::core::fmt::Debug)] pub(crate) enum Bar { V1Alpha1(v1alpha1::Bar), V1(v1::Bar), @@ -671,11 +727,8 @@ impl Bar { })?; match (current_object, desired_api_version) { (Self::V1Alpha1(__sv_bar), BarVersion::V1) => { - let converted: v1::BarSpec = __sv_bar.spec.into(); - let desired_object = Self::V1(v1::Bar { - metadata: __sv_bar.metadata, - spec: converted, - }); + let converted: v1::Bar = __sv_bar.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -684,12 +737,9 @@ impl Bar { converted_objects.push(desired_object); } (Self::V1Alpha1(__sv_bar), BarVersion::V2Alpha1) => { - let converted: v1::BarSpec = __sv_bar.spec.into(); - let converted: v2alpha1::BarSpec = converted.into(); - let desired_object = Self::V2Alpha1(v2alpha1::Bar { - metadata: __sv_bar.metadata, - spec: converted, - }); + let converted: v1::Bar = __sv_bar.into(); + let converted: v2alpha1::Bar = converted.into(); + let desired_object = Self::V2Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -698,11 +748,8 @@ impl Bar { converted_objects.push(desired_object); } (Self::V1(__sv_bar), BarVersion::V1Alpha1) => { - let converted: v1alpha1::BarSpec = __sv_bar.spec.into(); - let desired_object = Self::V1Alpha1(v1alpha1::Bar { - metadata: __sv_bar.metadata, - spec: converted, - }); + let converted: v1alpha1::Bar = __sv_bar.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -711,11 +758,8 @@ impl Bar { converted_objects.push(desired_object); } (Self::V1(__sv_bar), BarVersion::V2Alpha1) => { - let converted: v2alpha1::BarSpec = __sv_bar.spec.into(); - let desired_object = Self::V2Alpha1(v2alpha1::Bar { - metadata: __sv_bar.metadata, - spec: converted, - }); + let converted: v2alpha1::Bar = __sv_bar.into(); + let desired_object = Self::V2Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -724,12 +768,9 @@ impl Bar { converted_objects.push(desired_object); } (Self::V2Alpha1(__sv_bar), BarVersion::V1Alpha1) => { - let converted: v1::BarSpec = __sv_bar.spec.into(); - let converted: v1alpha1::BarSpec = converted.into(); - let desired_object = Self::V1Alpha1(v1alpha1::Bar { - metadata: __sv_bar.metadata, - spec: converted, - }); + let converted: v1::Bar = __sv_bar.into(); + let converted: v1alpha1::Bar = converted.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -738,11 +779,8 @@ impl Bar { converted_objects.push(desired_object); } (Self::V2Alpha1(__sv_bar), BarVersion::V1) => { - let converted: v1::BarSpec = __sv_bar.spec.into(); - let desired_object = Self::V1(v1::Bar { - metadata: __sv_bar.metadata, - spec: converted, - }); + let converted: v1::Bar = __sv_bar.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -758,24 +796,24 @@ impl Bar { fn from_json_object( object_value: ::serde_json::Value, ) -> ::std::result::Result { - let object_kind = object_value + let kind = object_value .get("kind") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "kind".to_owned(), })? .as_str() .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { field: "kind".to_owned(), })?; - if object_kind != "Bar" { + if kind != "Bar" { return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { - kind: object_kind.to_owned(), + kind: kind.to_owned(), expected: "Bar".to_owned(), }); } let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "apiVersion".to_owned(), })? .as_str() @@ -823,17 +861,17 @@ impl Bar { } } #[automatically_derived] -#[derive(Copy, Clone, Debug)] +#[derive(::core::marker::Copy, ::core::clone::Clone, ::core::fmt::Debug)] pub(crate) enum BarVersion { V1Alpha1, V1, V2Alpha1, } #[automatically_derived] -impl ::std::fmt::Display for BarVersion { +impl ::core::fmt::Display for BarVersion { fn fmt( &self, - f: &mut ::std::fmt::Formatter<'_>, + f: &mut ::core::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { f.write_str(self.as_version_str()) } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module_preserve.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@module_preserve.rs.snap similarity index 83% rename from crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module_preserve.rs.snap rename to crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@module_preserve.rs.snap index 40f611cc6..b5af49e8d 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module_preserve.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@module_preserve.rs.snap @@ -1,7 +1,7 @@ --- source: crates/stackable-versioned-macros/src/lib.rs expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/k8s/pass/module_preserve.rs +input_file: crates/stackable-versioned-macros/tests/inputs/pass/module_preserve.rs --- #[automatically_derived] pub(crate) mod versioned { @@ -51,17 +51,33 @@ pub(crate) mod versioned { Shaq, } } - impl ::std::convert::From for v1::Baz { + impl ::core::convert::From for v1::Baz { fn from(__sv_baz: v1alpha1::Baz) -> Self { Self { boom: __sv_baz.boom.into() } } } - impl ::std::convert::From for v1alpha1::Baz { + impl ::core::convert::From for v1alpha1::Baz { fn from(__sv_baz: v1::Baz) -> Self { Self { boom: __sv_baz.boom.into() } } } - impl ::std::convert::From for v1::FooSpec { + impl ::core::convert::From for v1::Foo { + fn from(__sv_foo: v1alpha1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } + } + impl ::core::convert::From for v1alpha1::Foo { + fn from(__sv_foo: v1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } + } + impl ::core::convert::From for v1::FooSpec { fn from(__sv_foospec: v1alpha1::FooSpec) -> Self { Self { bar: __sv_foospec.bar.into(), @@ -70,7 +86,7 @@ pub(crate) mod versioned { } } } - impl ::std::convert::From for v1alpha1::FooSpec { + impl ::core::convert::From for v1alpha1::FooSpec { fn from(__sv_foospec: v1::FooSpec) -> Self { Self { bar: __sv_foospec.bar.into(), @@ -78,21 +94,37 @@ pub(crate) mod versioned { } } } - impl ::std::convert::From for v1::BarSpec { + impl ::core::convert::From for v1::Bar { + fn from(__sv_bar: v1alpha1::Bar) -> Self { + Self { + metadata: __sv_bar.metadata, + spec: __sv_bar.spec.into(), + } + } + } + impl ::core::convert::From for v1alpha1::Bar { + fn from(__sv_bar: v1::Bar) -> Self { + Self { + metadata: __sv_bar.metadata, + spec: __sv_bar.spec.into(), + } + } + } + impl ::core::convert::From for v1::BarSpec { fn from(__sv_barspec: v1alpha1::BarSpec) -> Self { Self { baz: __sv_barspec.baz.into(), } } } - impl ::std::convert::From for v1alpha1::BarSpec { + impl ::core::convert::From for v1alpha1::BarSpec { fn from(__sv_barspec: v1::BarSpec) -> Self { Self { baz: __sv_barspec.baz.into(), } } } - impl ::std::convert::From for v1::Boom { + impl ::core::convert::From for v1::Boom { fn from(__sv_boom: v1alpha1::Boom) -> Self { match __sv_boom { v1alpha1::Boom::Big => v1::Boom::Big, @@ -100,7 +132,7 @@ pub(crate) mod versioned { } } } - impl ::std::convert::From for v1alpha1::Boom { + impl ::core::convert::From for v1alpha1::Boom { fn from(__sv_boom: v1::Boom) -> Self { match __sv_boom { v1::Boom::Big => v1alpha1::Boom::Big, @@ -150,18 +182,34 @@ pub(crate) mod versioned { Shaq, } } - impl ::std::convert::From for v2alpha1::Baz { + impl ::core::convert::From for v2alpha1::Baz { fn from(__sv_baz: v1::Baz) -> Self { Self { boom: __sv_baz.boom.into() } } } - impl ::std::convert::From for v1::Baz { + impl ::core::convert::From for v1::Baz { fn from(__sv_baz: v2alpha1::Baz) -> Self { Self { boom: __sv_baz.boom.into() } } } + impl ::core::convert::From for v2alpha1::Foo { + fn from(__sv_foo: v1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } + } + impl ::core::convert::From for v1::Foo { + fn from(__sv_foo: v2alpha1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } + } #[allow(deprecated)] - impl ::std::convert::From for v2alpha1::FooSpec { + impl ::core::convert::From for v2alpha1::FooSpec { fn from(__sv_foospec: v1::FooSpec) -> Self { Self { bar: __sv_foospec.bar.into(), @@ -171,7 +219,7 @@ pub(crate) mod versioned { } } #[allow(deprecated)] - impl ::std::convert::From for v1::FooSpec { + impl ::core::convert::From for v1::FooSpec { fn from(__sv_foospec: v2alpha1::FooSpec) -> Self { Self { bar: __sv_foospec.bar.into(), @@ -180,21 +228,37 @@ pub(crate) mod versioned { } } } - impl ::std::convert::From for v2alpha1::BarSpec { + impl ::core::convert::From for v2alpha1::Bar { + fn from(__sv_bar: v1::Bar) -> Self { + Self { + metadata: __sv_bar.metadata, + spec: __sv_bar.spec.into(), + } + } + } + impl ::core::convert::From for v1::Bar { + fn from(__sv_bar: v2alpha1::Bar) -> Self { + Self { + metadata: __sv_bar.metadata, + spec: __sv_bar.spec.into(), + } + } + } + impl ::core::convert::From for v2alpha1::BarSpec { fn from(__sv_barspec: v1::BarSpec) -> Self { Self { baz: __sv_barspec.baz.into(), } } } - impl ::std::convert::From for v1::BarSpec { + impl ::core::convert::From for v1::BarSpec { fn from(__sv_barspec: v2alpha1::BarSpec) -> Self { Self { baz: __sv_barspec.baz.into(), } } } - impl ::std::convert::From for v2alpha1::Boom { + impl ::core::convert::From for v2alpha1::Boom { fn from(__sv_boom: v1::Boom) -> Self { match __sv_boom { v1::Boom::Big => v2alpha1::Boom::Big, @@ -202,7 +266,7 @@ pub(crate) mod versioned { } } } - impl ::std::convert::From for v1::Boom { + impl ::core::convert::From for v1::Boom { fn from(__sv_boom: v2alpha1::Boom) -> Self { match __sv_boom { v2alpha1::Boom::Big => v1::Boom::Big, @@ -258,6 +322,7 @@ pub(crate) mod versioned { Shaq, } } + #[derive(::core::fmt::Debug)] pub enum Foo { V1Alpha1(v1alpha1::Foo), V1(v1::Foo), @@ -355,11 +420,8 @@ pub(crate) mod versioned { })?; match (current_object, desired_api_version) { (Self::V1Alpha1(__sv_foo), FooVersion::V1) => { - let converted: v1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1(v1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1::Foo = __sv_foo.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -368,12 +430,9 @@ pub(crate) mod versioned { converted_objects.push(desired_object); } (Self::V1Alpha1(__sv_foo), FooVersion::V2Alpha1) => { - let converted: v1::FooSpec = __sv_foo.spec.into(); - let converted: v2alpha1::FooSpec = converted.into(); - let desired_object = Self::V2Alpha1(v2alpha1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1::Foo = __sv_foo.into(); + let converted: v2alpha1::Foo = converted.into(); + let desired_object = Self::V2Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -382,11 +441,8 @@ pub(crate) mod versioned { converted_objects.push(desired_object); } (Self::V1(__sv_foo), FooVersion::V1Alpha1) => { - let converted: v1alpha1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1Alpha1(v1alpha1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1alpha1::Foo = __sv_foo.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -395,11 +451,8 @@ pub(crate) mod versioned { converted_objects.push(desired_object); } (Self::V1(__sv_foo), FooVersion::V2Alpha1) => { - let converted: v2alpha1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V2Alpha1(v2alpha1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v2alpha1::Foo = __sv_foo.into(); + let desired_object = Self::V2Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -408,12 +461,9 @@ pub(crate) mod versioned { converted_objects.push(desired_object); } (Self::V2Alpha1(__sv_foo), FooVersion::V1Alpha1) => { - let converted: v1::FooSpec = __sv_foo.spec.into(); - let converted: v1alpha1::FooSpec = converted.into(); - let desired_object = Self::V1Alpha1(v1alpha1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1::Foo = __sv_foo.into(); + let converted: v1alpha1::Foo = converted.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -422,11 +472,8 @@ pub(crate) mod versioned { converted_objects.push(desired_object); } (Self::V2Alpha1(__sv_foo), FooVersion::V1) => { - let converted: v1::FooSpec = __sv_foo.spec.into(); - let desired_object = Self::V1(v1::Foo { - metadata: __sv_foo.metadata, - spec: converted, - }); + let converted: v1::Foo = __sv_foo.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -442,24 +489,24 @@ pub(crate) mod versioned { fn from_json_object( object_value: ::serde_json::Value, ) -> ::std::result::Result { - let object_kind = object_value + let kind = object_value .get("kind") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "kind".to_owned(), })? .as_str() .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { field: "kind".to_owned(), })?; - if object_kind != "Foo" { + if kind != "Foo" { return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { - kind: object_kind.to_owned(), + kind: kind.to_owned(), expected: "Foo".to_owned(), }); } let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "apiVersion".to_owned(), })? .as_str() @@ -506,16 +553,16 @@ pub(crate) mod versioned { } } } - #[derive(Copy, Clone, Debug)] + #[derive(::core::marker::Copy, ::core::clone::Clone, ::core::fmt::Debug)] pub enum FooVersion { V1Alpha1, V1, V2Alpha1, } - impl ::std::fmt::Display for FooVersion { + impl ::core::fmt::Display for FooVersion { fn fmt( &self, - f: &mut ::std::fmt::Formatter<'_>, + f: &mut ::core::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { f.write_str(self.as_version_str()) } @@ -550,6 +597,7 @@ pub(crate) mod versioned { } } } + #[derive(::core::fmt::Debug)] pub enum Bar { V1Alpha1(v1alpha1::Bar), V1(v1::Bar), @@ -647,11 +695,8 @@ pub(crate) mod versioned { })?; match (current_object, desired_api_version) { (Self::V1Alpha1(__sv_bar), BarVersion::V1) => { - let converted: v1::BarSpec = __sv_bar.spec.into(); - let desired_object = Self::V1(v1::Bar { - metadata: __sv_bar.metadata, - spec: converted, - }); + let converted: v1::Bar = __sv_bar.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -660,12 +705,9 @@ pub(crate) mod versioned { converted_objects.push(desired_object); } (Self::V1Alpha1(__sv_bar), BarVersion::V2Alpha1) => { - let converted: v1::BarSpec = __sv_bar.spec.into(); - let converted: v2alpha1::BarSpec = converted.into(); - let desired_object = Self::V2Alpha1(v2alpha1::Bar { - metadata: __sv_bar.metadata, - spec: converted, - }); + let converted: v1::Bar = __sv_bar.into(); + let converted: v2alpha1::Bar = converted.into(); + let desired_object = Self::V2Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -674,11 +716,8 @@ pub(crate) mod versioned { converted_objects.push(desired_object); } (Self::V1(__sv_bar), BarVersion::V1Alpha1) => { - let converted: v1alpha1::BarSpec = __sv_bar.spec.into(); - let desired_object = Self::V1Alpha1(v1alpha1::Bar { - metadata: __sv_bar.metadata, - spec: converted, - }); + let converted: v1alpha1::Bar = __sv_bar.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -687,11 +726,8 @@ pub(crate) mod versioned { converted_objects.push(desired_object); } (Self::V1(__sv_bar), BarVersion::V2Alpha1) => { - let converted: v2alpha1::BarSpec = __sv_bar.spec.into(); - let desired_object = Self::V2Alpha1(v2alpha1::Bar { - metadata: __sv_bar.metadata, - spec: converted, - }); + let converted: v2alpha1::Bar = __sv_bar.into(); + let desired_object = Self::V2Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -700,12 +736,9 @@ pub(crate) mod versioned { converted_objects.push(desired_object); } (Self::V2Alpha1(__sv_bar), BarVersion::V1Alpha1) => { - let converted: v1::BarSpec = __sv_bar.spec.into(); - let converted: v1alpha1::BarSpec = converted.into(); - let desired_object = Self::V1Alpha1(v1alpha1::Bar { - metadata: __sv_bar.metadata, - spec: converted, - }); + let converted: v1::Bar = __sv_bar.into(); + let converted: v1alpha1::Bar = converted.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -714,11 +747,8 @@ pub(crate) mod versioned { converted_objects.push(desired_object); } (Self::V2Alpha1(__sv_bar), BarVersion::V1) => { - let converted: v1::BarSpec = __sv_bar.spec.into(); - let desired_object = Self::V1(v1::Bar { - metadata: __sv_bar.metadata, - spec: converted, - }); + let converted: v1::Bar = __sv_bar.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -734,24 +764,24 @@ pub(crate) mod versioned { fn from_json_object( object_value: ::serde_json::Value, ) -> ::std::result::Result { - let object_kind = object_value + let kind = object_value .get("kind") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "kind".to_owned(), })? .as_str() .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { field: "kind".to_owned(), })?; - if object_kind != "Bar" { + if kind != "Bar" { return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { - kind: object_kind.to_owned(), + kind: kind.to_owned(), expected: "Bar".to_owned(), }); } let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "apiVersion".to_owned(), })? .as_str() @@ -798,16 +828,16 @@ pub(crate) mod versioned { } } } - #[derive(Copy, Clone, Debug)] + #[derive(::core::marker::Copy, ::core::clone::Clone, ::core::fmt::Debug)] pub enum BarVersion { V1Alpha1, V1, V2Alpha1, } - impl ::std::fmt::Display for BarVersion { + impl ::core::fmt::Display for BarVersion { fn fmt( &self, - f: &mut ::std::fmt::Formatter<'_>, + f: &mut ::core::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { f.write_str(self.as_version_str()) } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@rename.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@renamed_field.rs.snap similarity index 79% rename from crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@rename.rs.snap rename to crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@renamed_field.rs.snap index 90b9db812..43ce07599 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@rename.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@renamed_field.rs.snap @@ -1,7 +1,7 @@ --- source: crates/stackable-versioned-macros/src/lib.rs expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/rename.rs +input_file: crates/stackable-versioned-macros/tests/inputs/pass/renamed_field.rs --- #[automatically_derived] mod v1alpha1 { @@ -12,7 +12,7 @@ mod v1alpha1 { } } #[automatically_derived] -impl ::std::convert::From for v1beta1::Foo { +impl ::core::convert::From for v1beta1::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { Self { bar: __sv_foo.bat.into(), @@ -21,7 +21,7 @@ impl ::std::convert::From for v1beta1::Foo { } } #[automatically_derived] -impl ::std::convert::From for v1alpha1::Foo { +impl ::core::convert::From for v1alpha1::Foo { fn from(__sv_foo: v1beta1::Foo) -> Self { Self { bat: __sv_foo.bar.into(), @@ -38,7 +38,7 @@ mod v1beta1 { } } #[automatically_derived] -impl ::std::convert::From for v1::Foo { +impl ::core::convert::From for v1::Foo { fn from(__sv_foo: v1beta1::Foo) -> Self { Self { bar: __sv_foo.bar.into(), @@ -47,7 +47,7 @@ impl ::std::convert::From for v1::Foo { } } #[automatically_derived] -impl ::std::convert::From for v1beta1::Foo { +impl ::core::convert::From for v1beta1::Foo { fn from(__sv_foo: v1::Foo) -> Self { Self { bar: __sv_foo.bar.into(), diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@renamed_kind.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@renamed_kind.rs.snap similarity index 82% rename from crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@renamed_kind.rs.snap rename to crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@renamed_kind.rs.snap index 899658557..dbb5e5ead 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@renamed_kind.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@renamed_kind.rs.snap @@ -1,7 +1,7 @@ --- source: crates/stackable-versioned-macros/src/lib.rs expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/k8s/pass/renamed_kind.rs +input_file: crates/stackable-versioned-macros/tests/inputs/pass/renamed_kind.rs --- #[automatically_derived] pub mod v1alpha1 { @@ -20,7 +20,25 @@ pub mod v1alpha1 { } } #[automatically_derived] -impl ::std::convert::From for v1beta1::FooSpec { +impl ::core::convert::From for v1beta1::FooBar { + fn from(__sv_foobar: v1alpha1::FooBar) -> Self { + Self { + metadata: __sv_foobar.metadata, + spec: __sv_foobar.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1alpha1::FooBar { + fn from(__sv_foobar: v1beta1::FooBar) -> Self { + Self { + metadata: __sv_foobar.metadata, + spec: __sv_foobar.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1beta1::FooSpec { fn from(__sv_foospec: v1alpha1::FooSpec) -> Self { Self { bah: ::std::default::Default::default(), @@ -29,7 +47,7 @@ impl ::std::convert::From for v1beta1::FooSpec { } } #[automatically_derived] -impl ::std::convert::From for v1alpha1::FooSpec { +impl ::core::convert::From for v1alpha1::FooSpec { fn from(__sv_foospec: v1beta1::FooSpec) -> Self { Self { baz: __sv_foospec.baz.into(), @@ -54,7 +72,25 @@ pub mod v1beta1 { } } #[automatically_derived] -impl ::std::convert::From for v1::FooSpec { +impl ::core::convert::From for v1::FooBar { + fn from(__sv_foobar: v1beta1::FooBar) -> Self { + Self { + metadata: __sv_foobar.metadata, + spec: __sv_foobar.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1beta1::FooBar { + fn from(__sv_foobar: v1::FooBar) -> Self { + Self { + metadata: __sv_foobar.metadata, + spec: __sv_foobar.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1::FooSpec { fn from(__sv_foospec: v1beta1::FooSpec) -> Self { Self { bar: __sv_foospec.bah.into(), @@ -63,7 +99,7 @@ impl ::std::convert::From for v1::FooSpec { } } #[automatically_derived] -impl ::std::convert::From for v1beta1::FooSpec { +impl ::core::convert::From for v1beta1::FooSpec { fn from(__sv_foospec: v1::FooSpec) -> Self { Self { bah: __sv_foospec.bar.into(), @@ -89,6 +125,7 @@ pub mod v1 { } } #[automatically_derived] +#[derive(::core::fmt::Debug)] pub enum FooBar { V1Alpha1(v1alpha1::FooBar), V1Beta1(v1beta1::FooBar), @@ -187,11 +224,8 @@ impl FooBar { })?; match (current_object, desired_api_version) { (Self::V1Alpha1(__sv_foobar), FooBarVersion::V1Beta1) => { - let converted: v1beta1::FooSpec = __sv_foobar.spec.into(); - let desired_object = Self::V1Beta1(v1beta1::FooBar { - metadata: __sv_foobar.metadata, - spec: converted, - }); + let converted: v1beta1::FooBar = __sv_foobar.into(); + let desired_object = Self::V1Beta1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -200,12 +234,9 @@ impl FooBar { converted_objects.push(desired_object); } (Self::V1Alpha1(__sv_foobar), FooBarVersion::V1) => { - let converted: v1beta1::FooSpec = __sv_foobar.spec.into(); - let converted: v1::FooSpec = converted.into(); - let desired_object = Self::V1(v1::FooBar { - metadata: __sv_foobar.metadata, - spec: converted, - }); + let converted: v1beta1::FooBar = __sv_foobar.into(); + let converted: v1::FooBar = converted.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -214,11 +245,8 @@ impl FooBar { converted_objects.push(desired_object); } (Self::V1Beta1(__sv_foobar), FooBarVersion::V1Alpha1) => { - let converted: v1alpha1::FooSpec = __sv_foobar.spec.into(); - let desired_object = Self::V1Alpha1(v1alpha1::FooBar { - metadata: __sv_foobar.metadata, - spec: converted, - }); + let converted: v1alpha1::FooBar = __sv_foobar.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -227,11 +255,8 @@ impl FooBar { converted_objects.push(desired_object); } (Self::V1Beta1(__sv_foobar), FooBarVersion::V1) => { - let converted: v1::FooSpec = __sv_foobar.spec.into(); - let desired_object = Self::V1(v1::FooBar { - metadata: __sv_foobar.metadata, - spec: converted, - }); + let converted: v1::FooBar = __sv_foobar.into(); + let desired_object = Self::V1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -240,12 +265,9 @@ impl FooBar { converted_objects.push(desired_object); } (Self::V1(__sv_foobar), FooBarVersion::V1Alpha1) => { - let converted: v1beta1::FooSpec = __sv_foobar.spec.into(); - let converted: v1alpha1::FooSpec = converted.into(); - let desired_object = Self::V1Alpha1(v1alpha1::FooBar { - metadata: __sv_foobar.metadata, - spec: converted, - }); + let converted: v1beta1::FooBar = __sv_foobar.into(); + let converted: v1alpha1::FooBar = converted.into(); + let desired_object = Self::V1Alpha1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -254,11 +276,8 @@ impl FooBar { converted_objects.push(desired_object); } (Self::V1(__sv_foobar), FooBarVersion::V1Beta1) => { - let converted: v1beta1::FooSpec = __sv_foobar.spec.into(); - let desired_object = Self::V1Beta1(v1beta1::FooBar { - metadata: __sv_foobar.metadata, - spec: converted, - }); + let converted: v1beta1::FooBar = __sv_foobar.into(); + let desired_object = Self::V1Beta1(converted); let desired_object = desired_object .into_json_value() .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { @@ -274,24 +293,24 @@ impl FooBar { fn from_json_object( object_value: ::serde_json::Value, ) -> ::std::result::Result { - let object_kind = object_value + let kind = object_value .get("kind") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "kind".to_owned(), })? .as_str() .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { field: "kind".to_owned(), })?; - if object_kind != "FooBar" { + if kind != "FooBar" { return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { - kind: object_kind.to_owned(), + kind: kind.to_owned(), expected: "FooBar".to_owned(), }); } let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "apiVersion".to_owned(), })? .as_str() @@ -339,17 +358,17 @@ impl FooBar { } } #[automatically_derived] -#[derive(Copy, Clone, Debug)] +#[derive(::core::marker::Copy, ::core::clone::Clone, ::core::fmt::Debug)] pub enum FooBarVersion { V1Alpha1, V1Beta1, V1, } #[automatically_derived] -impl ::std::fmt::Display for FooBarVersion { +impl ::core::fmt::Display for FooBarVersion { fn fmt( &self, - f: &mut ::std::fmt::Formatter<'_>, + f: &mut ::core::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { f.write_str(self.as_version_str()) } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@shortnames.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@shortnames.rs.snap similarity index 94% rename from crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@shortnames.rs.snap rename to crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@shortnames.rs.snap index 6d0d019fc..501588df8 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@shortnames.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@shortnames.rs.snap @@ -1,7 +1,7 @@ --- source: crates/stackable-versioned-macros/src/lib.rs expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/k8s/pass/shortnames.rs +input_file: crates/stackable-versioned-macros/tests/inputs/pass/shortnames.rs --- #[automatically_derived] pub(crate) mod v1alpha1 { @@ -24,6 +24,7 @@ pub(crate) mod v1alpha1 { pub struct FooSpec {} } #[automatically_derived] +#[derive(::core::fmt::Debug)] pub(crate) enum Foo { V1Alpha1(v1alpha1::Foo), } @@ -123,24 +124,24 @@ impl Foo { fn from_json_object( object_value: ::serde_json::Value, ) -> ::std::result::Result { - let object_kind = object_value + let kind = object_value .get("kind") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "kind".to_owned(), })? .as_str() .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { field: "kind".to_owned(), })?; - if object_kind != "Foo" { + if kind != "Foo" { return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { - kind: object_kind.to_owned(), + kind: kind.to_owned(), expected: "Foo".to_owned(), }); } let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { field: "apiVersion".to_owned(), })? .as_str() @@ -172,15 +173,15 @@ impl Foo { } } #[automatically_derived] -#[derive(Copy, Clone, Debug)] +#[derive(::core::marker::Copy, ::core::clone::Clone, ::core::fmt::Debug)] pub(crate) enum FooVersion { V1Alpha1, } #[automatically_derived] -impl ::std::fmt::Display for FooVersion { +impl ::core::fmt::Display for FooVersion { fn fmt( &self, - f: &mut ::std::fmt::Formatter<'_>, + f: &mut ::core::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { f.write_str(self.as_version_str()) } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@submodule.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@submodule.rs.snap similarity index 75% rename from crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@submodule.rs.snap rename to crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@submodule.rs.snap index 76176c894..913a9eca8 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__default@submodule.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@submodule.rs.snap @@ -1,7 +1,7 @@ --- source: crates/stackable-versioned-macros/src/lib.rs expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/default/pass/submodule.rs +input_file: crates/stackable-versioned-macros/tests/inputs/pass/submodule.rs --- #[automatically_derived] mod v1alpha1 { @@ -13,13 +13,13 @@ mod v1alpha1 { } } #[automatically_derived] -impl ::std::convert::From for v1::Foo { +impl ::core::convert::From for v1::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { Self { bar: __sv_foo.bar.into() } } } #[automatically_derived] -impl ::std::convert::From for v1alpha1::Foo { +impl ::core::convert::From for v1alpha1::Foo { fn from(__sv_foo: v1::Foo) -> Self { Self { bar: __sv_foo.bar.into() } } From 7bfc2bbcc957c5d1dd611e102751814aa8e5720f Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 4 Jul 2025 15:43:48 +0200 Subject: [PATCH 12/16] test(stackable-versioned): Adjust existing integration tests --- .../stackable-versioned/tests/conversions.rs | 19 +-- .../conversions/pass/persons_to_v1alpha1.json | 50 ++++--- .../conversions/pass/persons_to_v3.json | 28 +++- crates/stackable-versioned/tests/person.rs | 98 +++++++++----- ...rsions__pass@persons_to_v1alpha1.json.snap | 125 +++++++++++++++++- .../conversions__pass@persons_to_v3.json.snap | 44 ++++++ 6 files changed, 288 insertions(+), 76 deletions(-) diff --git a/crates/stackable-versioned/tests/conversions.rs b/crates/stackable-versioned/tests/conversions.rs index c5bb0e337..c04a767da 100644 --- a/crates/stackable-versioned/tests/conversions.rs +++ b/crates/stackable-versioned/tests/conversions.rs @@ -1,16 +1,12 @@ -use std::{fs::File, path::Path}; - use insta::{assert_snapshot, glob}; -use kube::core::{conversion::ConversionReview, response::StatusSummary}; - -use crate::person::Person; +use kube::core::response::StatusSummary; mod person; #[test] fn pass() { glob!("./inputs/conversions/pass/", "*.json", |path| { - let (request, response) = convert_via_file(path); + let (request, response) = person::convert_via_file(path); let formatted = serde_json::to_string_pretty(&response) .expect("Failed to serialize ConversionResponse"); @@ -32,7 +28,7 @@ fn pass() { #[test] fn fail() { glob!("./inputs/conversions/fail/", "*.json", |path| { - let (request, response) = convert_via_file(path); + let (request, response) = person::convert_via_file(path); let formatted = serde_json::to_string_pretty(&response) .expect("Failed to serialize ConversionResponse"); @@ -52,12 +48,3 @@ fn fail() { } }) } - -fn convert_via_file(path: &Path) -> (ConversionReview, ConversionReview) { - let request: ConversionReview = - serde_json::from_reader(File::open(path).expect("failed to open test file")) - .expect("failed to parse ConversionReview from test file"); - let response = Person::try_convert(request.clone()); - - (request, response) -} diff --git a/crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v1alpha1.json b/crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v1alpha1.json index 2a0529763..93b31963b 100644 --- a/crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v1alpha1.json +++ b/crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v1alpha1.json @@ -9,12 +9,15 @@ "apiVersion": "test.stackable.tech/v1alpha1", "kind": "Person", "metadata": { - "name": "sbernauer", + "name": "jdoe", "namespace": "default", "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" }, "spec": { - "username": "sbernauer" + "username": "jdoe", + "socials": { + "email": "jdoe@example.com" + } } }, { @@ -22,9 +25,12 @@ "kind": "Person", "metadata": {}, "spec": { - "username": "sbernauer", - "firstName": "Sebastian", - "lastName": "Bernauer" + "username": "jdoe", + "firstName": "John", + "lastName": "Doe", + "socials": { + "email": "jdoe@example.com" + } } }, { @@ -32,9 +38,13 @@ "kind": "Person", "metadata": {}, "spec": { - "username": "sbernauer", - "firstName": "Sebastian", - "lastName": "Bernauer" + "username": "jdoe", + "firstName": "John", + "lastName": "Doe", + "socials": { + "email": "jdoe@example.com", + "mastodon": "@jdoe@example.com" + } } }, { @@ -42,10 +52,14 @@ "kind": "Person", "metadata": {}, "spec": { - "username": "sbernauer", - "firstName": "Sebastian", - "lastName": "Bernauer", - "gender": "Male" + "username": "jdoe", + "firstName": "John", + "lastName": "Doe", + "gender": "Male", + "socials": { + "email": "jdoe@example.com", + "mastodon": "@jdoe@example.com" + } } }, { @@ -53,10 +67,14 @@ "kind": "Person", "metadata": {}, "spec": { - "username": "sbernauer", - "firstName": "Sebastian", - "lastName": "Bernauer", - "gender": "Male" + "username": "jdoe", + "firstName": "John", + "lastName": "Doe", + "gender": "Male", + "socials": { + "email": "jdoe@example.com", + "mastodon": "@jdoe@example.com" + } } } ] diff --git a/crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v3.json b/crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v3.json index 14848515e..5b15f9a7b 100644 --- a/crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v3.json +++ b/crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v3.json @@ -14,7 +14,10 @@ "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" }, "spec": { - "username": "sbernauer" + "username": "sbernauer", + "socials": { + "email": "jdoe@example.com" + } } }, { @@ -24,7 +27,10 @@ "spec": { "username": "sbernauer", "firstName": "Sebastian", - "lastName": "Bernauer" + "lastName": "Bernauer", + "socials": { + "email": "jdoe@example.com" + } } }, { @@ -34,7 +40,11 @@ "spec": { "username": "sbernauer", "firstName": "Sebastian", - "lastName": "Bernauer" + "lastName": "Bernauer", + "socials": { + "email": "jdoe@example.com", + "mastodon": "@jdoe@example.com" + } } }, { @@ -45,7 +55,11 @@ "username": "sbernauer", "firstName": "Sebastian", "lastName": "Bernauer", - "gender": "Male" + "gender": "Male", + "socials": { + "email": "jdoe@example.com", + "mastodon": "@jdoe@example.com" + } } }, { @@ -56,7 +70,11 @@ "username": "sbernauer", "firstName": "Sebastian", "lastName": "Bernauer", - "gender": "Male" + "gender": "Male", + "socials": { + "email": "jdoe@example.com", + "mastodon": "@jdoe@example.com" + } } } ] diff --git a/crates/stackable-versioned/tests/person.rs b/crates/stackable-versioned/tests/person.rs index f2610fa60..346c3cf90 100644 --- a/crates/stackable-versioned/tests/person.rs +++ b/crates/stackable-versioned/tests/person.rs @@ -1,50 +1,78 @@ -use kube::CustomResource; +use std::{fs::File, path::Path}; + +use kube::{CustomResource, core::conversion::ConversionReview}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use stackable_versioned::versioned; +// Fixes an error with this function being marked as unused. See +// - https://stackoverflow.com/a/67902444 +// - https://github.com/rust-lang/rust/issues/46379 +#[allow(dead_code)] +pub fn convert_via_file(path: &Path) -> (ConversionReview, ConversionReview) { + let request: ConversionReview = + serde_json::from_reader(File::open(path).expect("failed to open test file")) + .expect("failed to parse ConversionReview from test file"); + let response = Person::try_convert(request.clone()); + + (request, response) +} + #[versioned( - k8s(group = "test.stackable.tech",), version(name = "v1alpha1"), version(name = "v1alpha2"), version(name = "v1beta1"), version(name = "v2"), - version(name = "v3") -)] -#[derive( - Clone, - Debug, - Eq, - Hash, - Ord, - PartialEq, - PartialOrd, - CustomResource, - Deserialize, - JsonSchema, - Serialize, + version(name = "v3"), + options(k8s(experimental_conversion_tracking)) )] -#[serde(rename_all = "camelCase")] -pub struct PersonSpec { - username: String, +pub mod versioned { + #[versioned(crd(group = "test.stackable.tech"))] + #[derive( + Clone, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, + CustomResource, + Deserialize, + JsonSchema, + Serialize, + )] + #[serde(rename_all = "camelCase")] + pub struct PersonSpec { + username: String, + + // In v1alpha2 first and last name have been added + #[versioned(added(since = "v1alpha2"))] + first_name: String, - // In v1alpha2 first and last name have been added - #[versioned(added(since = "v1alpha2"))] - first_name: String, - #[versioned(added(since = "v1alpha2"))] - last_name: String, + #[versioned(added(since = "v1alpha2"))] + last_name: String, - // We started out with a enum. As we *need* to provide a default, we have a Unknown variant. - // Afterwards we figured let's be more flexible and accept any arbitrary String. - #[versioned( - added(since = "v2", default = "default_gender"), - changed(since = "v3", from_type = "Gender") + // We started out with a enum. As we *need* to provide a default, we have a Unknown variant. + // Afterwards we figured let's be more flexible and accept any arbitrary String. + #[versioned( + added(since = "v2", default = "default_gender"), + changed(since = "v3", from_type = "Gender") + )] + gender: String, + + #[versioned(nested)] + socials: Socials, + } + + #[derive( + Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Deserialize, Serialize, JsonSchema, )] - gender: String, -} + pub struct Socials { + email: String, -fn default_gender() -> Gender { - Gender::Unknown + #[versioned(added(since = "v1beta1"))] + mastodon: String, + } } #[derive( @@ -57,6 +85,10 @@ pub enum Gender { Female, } +fn default_gender() -> Gender { + Gender::Unknown +} + impl From for String { fn from(value: Gender) -> Self { match value { diff --git a/crates/stackable-versioned/tests/snapshots/conversions__pass@persons_to_v1alpha1.json.snap b/crates/stackable-versioned/tests/snapshots/conversions__pass@persons_to_v1alpha1.json.snap index ced5fb427..1488b4dfe 100644 --- a/crates/stackable-versioned/tests/snapshots/conversions__pass@persons_to_v1alpha1.json.snap +++ b/crates/stackable-versioned/tests/snapshots/conversions__pass@persons_to_v1alpha1.json.snap @@ -16,12 +16,15 @@ input_file: crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_ "apiVersion": "test.stackable.tech/v1alpha1", "kind": "Person", "metadata": { - "name": "sbernauer", + "name": "jdoe", "namespace": "default", "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" }, "spec": { - "username": "sbernauer" + "socials": { + "email": "jdoe@example.com" + }, + "username": "jdoe" } }, { @@ -29,7 +32,27 @@ input_file: crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_ "kind": "Person", "metadata": {}, "spec": { - "username": "sbernauer" + "socials": { + "email": "jdoe@example.com" + }, + "username": "jdoe" + }, + "status": { + "changedValues": { + "downgrades": {}, + "upgrades": { + "v1alpha2": [ + { + "fieldName": "first_name", + "value": "John" + }, + { + "fieldName": "last_name", + "value": "Doe" + } + ] + } + } } }, { @@ -37,7 +60,33 @@ input_file: crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_ "kind": "Person", "metadata": {}, "spec": { - "username": "sbernauer" + "socials": { + "email": "jdoe@example.com" + }, + "username": "jdoe" + }, + "status": { + "changedValues": { + "downgrades": {}, + "upgrades": { + "v1alpha2": [ + { + "fieldName": "first_name", + "value": "John" + }, + { + "fieldName": "last_name", + "value": "Doe" + } + ], + "v1beta1": [ + { + "fieldName": "socials.mastodon", + "value": "@jdoe@example.com" + } + ] + } + } } }, { @@ -45,7 +94,39 @@ input_file: crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_ "kind": "Person", "metadata": {}, "spec": { - "username": "sbernauer" + "socials": { + "email": "jdoe@example.com" + }, + "username": "jdoe" + }, + "status": { + "changedValues": { + "downgrades": {}, + "upgrades": { + "v1alpha2": [ + { + "fieldName": "first_name", + "value": "John" + }, + { + "fieldName": "last_name", + "value": "Doe" + } + ], + "v1beta1": [ + { + "fieldName": "socials.mastodon", + "value": "@jdoe@example.com" + } + ], + "v2": [ + { + "fieldName": "gender", + "value": "Male" + } + ] + } + } } }, { @@ -53,7 +134,39 @@ input_file: crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_ "kind": "Person", "metadata": {}, "spec": { - "username": "sbernauer" + "socials": { + "email": "jdoe@example.com" + }, + "username": "jdoe" + }, + "status": { + "changedValues": { + "downgrades": {}, + "upgrades": { + "v1alpha2": [ + { + "fieldName": "first_name", + "value": "John" + }, + { + "fieldName": "last_name", + "value": "Doe" + } + ], + "v1beta1": [ + { + "fieldName": "socials.mastodon", + "value": "@jdoe@example.com" + } + ], + "v2": [ + { + "fieldName": "gender", + "value": "Male" + } + ] + } + } } } ] diff --git a/crates/stackable-versioned/tests/snapshots/conversions__pass@persons_to_v3.json.snap b/crates/stackable-versioned/tests/snapshots/conversions__pass@persons_to_v3.json.snap index f6b9a7aec..4b07e62e7 100644 --- a/crates/stackable-versioned/tests/snapshots/conversions__pass@persons_to_v3.json.snap +++ b/crates/stackable-versioned/tests/snapshots/conversions__pass@persons_to_v3.json.snap @@ -24,7 +24,17 @@ input_file: crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_ "firstName": "", "gender": "Unknown", "lastName": "", + "socials": { + "email": "jdoe@example.com", + "mastodon": "" + }, "username": "sbernauer" + }, + "status": { + "changedValues": { + "downgrades": {}, + "upgrades": {} + } } }, { @@ -35,7 +45,17 @@ input_file: crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_ "firstName": "Sebastian", "gender": "Unknown", "lastName": "Bernauer", + "socials": { + "email": "jdoe@example.com", + "mastodon": "" + }, "username": "sbernauer" + }, + "status": { + "changedValues": { + "downgrades": {}, + "upgrades": {} + } } }, { @@ -46,7 +66,17 @@ input_file: crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_ "firstName": "Sebastian", "gender": "Unknown", "lastName": "Bernauer", + "socials": { + "email": "jdoe@example.com", + "mastodon": "@jdoe@example.com" + }, "username": "sbernauer" + }, + "status": { + "changedValues": { + "downgrades": {}, + "upgrades": {} + } } }, { @@ -57,7 +87,17 @@ input_file: crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_ "firstName": "Sebastian", "gender": "Male", "lastName": "Bernauer", + "socials": { + "email": "jdoe@example.com", + "mastodon": "@jdoe@example.com" + }, "username": "sbernauer" + }, + "status": { + "changedValues": { + "downgrades": {}, + "upgrades": {} + } } }, { @@ -68,6 +108,10 @@ input_file: crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_ "firstName": "Sebastian", "gender": "Male", "lastName": "Bernauer", + "socials": { + "email": "jdoe@example.com", + "mastodon": "@jdoe@example.com" + }, "username": "sbernauer" } } From 502e55d4a7561fcfe5f5046c8c8242b96aea658e Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 4 Jul 2025 16:51:10 +0200 Subject: [PATCH 13/16] test(stackable-versioned): Add conversion roundtrip integration test --- .../tests/inputs/roundtrip/person_v3.json | 31 +++++++++++ crates/stackable-versioned/tests/person.rs | 23 +++++++- crates/stackable-versioned/tests/roundtrip.rs | 54 +++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 crates/stackable-versioned/tests/inputs/roundtrip/person_v3.json create mode 100644 crates/stackable-versioned/tests/roundtrip.rs diff --git a/crates/stackable-versioned/tests/inputs/roundtrip/person_v3.json b/crates/stackable-versioned/tests/inputs/roundtrip/person_v3.json new file mode 100644 index 000000000..da6dceaaf --- /dev/null +++ b/crates/stackable-versioned/tests/inputs/roundtrip/person_v3.json @@ -0,0 +1,31 @@ +{ + "kind": "ConversionReview", + "apiVersion": "apiextensions.k8s.io/v1", + "request": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "desiredAPIVersion": "test.stackable.tech/v1alpha1", + "objects": [ + { + "apiVersion": "test.stackable.tech/v3", + "kind": "Person", + "metadata": {}, + "spec": { + "username": "jdoe", + "firstName": "John", + "lastName": "Doe", + "gender": "Male", + "socials": { + "email": "jdoe@example.com", + "mastodon": "@jdoe@example.com" + } + }, + "status": { + "changedValues": { + "downgrades": {}, + "upgrades": {} + } + } + } + ] + } +} diff --git a/crates/stackable-versioned/tests/person.rs b/crates/stackable-versioned/tests/person.rs index 346c3cf90..34ee28026 100644 --- a/crates/stackable-versioned/tests/person.rs +++ b/crates/stackable-versioned/tests/person.rs @@ -1,6 +1,9 @@ use std::{fs::File, path::Path}; -use kube::{CustomResource, core::conversion::ConversionReview}; +use kube::{ + CustomResource, + core::conversion::{ConversionRequest, ConversionReview}, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use stackable_versioned::versioned; @@ -18,6 +21,24 @@ pub fn convert_via_file(path: &Path) -> (ConversionReview, ConversionReview) { (request, response) } +#[allow(dead_code)] +pub fn roundtrip_conversion_review( + response_review: ConversionReview, + desired_api_version: PersonVersion, +) -> ConversionReview { + let response = response_review.response.unwrap(); + ConversionReview { + types: response_review.types, + request: Some(ConversionRequest { + desired_api_version: desired_api_version.as_api_version_str().to_owned(), + objects: response.converted_objects, + types: response.types, + uid: response.uid, + }), + response: None, + } +} + #[versioned( version(name = "v1alpha1"), version(name = "v1alpha2"), diff --git a/crates/stackable-versioned/tests/roundtrip.rs b/crates/stackable-versioned/tests/roundtrip.rs new file mode 100644 index 000000000..b1703d81b --- /dev/null +++ b/crates/stackable-versioned/tests/roundtrip.rs @@ -0,0 +1,54 @@ +use insta::glob; +use kube::core::response::StatusSummary; + +use crate::person::{Person, PersonVersion}; + +mod person; + +#[test] +fn person_v3_v1alpha1_v3() { + glob!("./inputs/roundtrip", "*.json", |path| { + // Convert from v3 to v1alpha1 + // NOTE (@Techassi): It should be noted that the input conversion review + // contains a status with empty changedValues to be able to assert_eq + // the objects at the end. As mentioned in the actual macro code, we + // should avoid "polluting" the status if it is empty. + let (request_v1alpha1, response_v1alpha1) = person::convert_via_file(path); + let response = response_v1alpha1 + .response + .as_ref() + .expect("v1alpha1 review must have a response"); + + assert_eq!(response.result.status, Some(StatusSummary::Success)); + + // Construct the roundtrip review + let roundtrip_review = + person::roundtrip_conversion_review(response_v1alpha1, PersonVersion::V3); + + // Convert back to v3 from v1alpha1 + let response_v3 = Person::try_convert(roundtrip_review); + let response = response_v3 + .response + .as_ref() + .expect("v3 review must have a response"); + + assert_eq!(response.result.status, Some(StatusSummary::Success)); + + // Now let compare the object how it started out with the object which + // was produced through the conversion roundtrip. They must match. + let original_object = request_v1alpha1 + .request + .as_ref() + .expect("v1alpha1 review must have a request") + .objects + .first() + .expect("there must be at least one object"); + + let converted_object = response + .converted_objects + .first() + .expect("there must be at least one object"); + + assert_eq!(original_object, converted_object); + }); +} From 0f8ed66446541d21a4968ed1cb171be2b83fed7d Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 4 Jul 2025 16:51:58 +0200 Subject: [PATCH 14/16] chore(stackable-versioned): Add FIXME comments --- .../src/codegen/container/struct/conversion.rs | 5 +++++ crates/stackable-versioned/src/lib.rs | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs index 4911a19ac..43f536f65 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs @@ -164,6 +164,11 @@ impl Struct { } }; + // FIXME (@Techassi): Both inserts and removals currently "leak" how the fields are named + // internally in Rust and not what they are serialized as (camelCase most of the time). This + // can be especially confusing for users who look at the status and see those field names. + // Ideally we would integrate with the serde(rename) functionality to produce these field + // names. let inserts = self.generate_tracking_inserts(direction, next_version, mod_gen_ctx); let removals = self.generate_tracking_removals(direction, next_version, mod_gen_ctx); let json_paths = self.generate_json_paths(next_version); diff --git a/crates/stackable-versioned/src/lib.rs b/crates/stackable-versioned/src/lib.rs index 8c36ef00f..7d4d50080 100644 --- a/crates/stackable-versioned/src/lib.rs +++ b/crates/stackable-versioned/src/lib.rs @@ -59,9 +59,10 @@ pub trait TrackingStatus { } // NOTE (@Techassi): This struct represents a rough first draft of how tracking values across -// CRD versions can be achieved. It is currently untested and unproven and might change down the -// line. Currently, this struct is only generated by the macro but not actually used by any other -// code. The tracking itself will be introduced in a follow-up PR. +// CRD versions can be achieved. It might change down the line. +// FIXME (@Techassi): Ideally we don't serialize empty maps. Further, we shouldn't serialize the +// changedValues field in the status, if there it is empty. This currently "pollutes" the status +// with empty JSON objects. /// Contains changed values during upgrades and downgrades of CRDs. #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] pub struct ChangedValues { From 277c2b1e6041e78447cca96d43b79144751b34ad Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 4 Jul 2025 16:53:02 +0200 Subject: [PATCH 15/16] chore(stackable-operator): Add required Default derive to ListenerStatus --- crates/stackable-operator/src/crd/listener/listeners/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/stackable-operator/src/crd/listener/listeners/mod.rs b/crates/stackable-operator/src/crd/listener/listeners/mod.rs index 1d2fdf93a..af4c5ccf7 100644 --- a/crates/stackable-operator/src/crd/listener/listeners/mod.rs +++ b/crates/stackable-operator/src/crd/listener/listeners/mod.rs @@ -112,7 +112,7 @@ pub mod versioned { } /// Informs users about how to reach the Listener. - #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] + #[derive(Serialize, Deserialize, Clone, Debug, Default, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ListenerStatus { /// The backing Kubernetes Service. From 045e980cd1cca74d34825a1133a33673b5fb9af3 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 4 Jul 2025 16:54:47 +0200 Subject: [PATCH 16/16] chore(stackable-versioned): Remove unused k8s-version dependency --- Cargo.lock | 1 - crates/stackable-versioned/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c89c0e3e1..1708af7fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3069,7 +3069,6 @@ version = "0.7.1" dependencies = [ "insta", "k8s-openapi", - "k8s-version", "kube", "schemars", "serde", diff --git a/crates/stackable-versioned/Cargo.toml b/crates/stackable-versioned/Cargo.toml index 31638486c..1b780e7b6 100644 --- a/crates/stackable-versioned/Cargo.toml +++ b/crates/stackable-versioned/Cargo.toml @@ -12,7 +12,6 @@ all-features = true [dependencies] stackable-versioned-macros = { path = "../stackable-versioned-macros" } -k8s-version = { path = "../k8s-version", features = ["serde"] } schemars.workspace = true serde.workspace = true