From f7735379fa51c7aaddf1c5032437fc50810b8dcf Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Thu, 5 Nov 2026 08:06:25 +0100 Subject: [PATCH 1/6] feat(versioned): Add support for the removed field action Fields can now be removed in a particular version using `#[versioned(removed(in = "..."))]`. The field is part of all versions before the removal and absent from that version onwards. During upgrades the value is dropped, during downgrades the field is initialized via the optional `default` argument (defaults to `Default::default`). Removal must be the last action of a field and cannot be combined with another action in the same version or happen in the first version. Deprecating the field beforehand is optional. Removing enum variants is not supported yet, as an upgrade would have no variant to map to. Removed values are not tracked yet when experimental_conversion_tracking is enabled. --- .../src/attrs/item/mod.rs | 103 ++- .../src/attrs/item/variant.rs | 9 + .../src/codegen/changes.rs | 20 +- .../codegen/container/struct/conversion.rs | 1 + .../src/codegen/item/field.rs | 27 +- .../src/codegen/item/mod.rs | 6 + .../src/codegen/item/variant.rs | 3 +- crates/stackable-versioned-macros/src/lib.rs | 120 +++- .../tests/inputs/fail/removed.rs | 35 + .../tests/inputs/fail/removed.stderr | 29 + .../pass/conversion_tracking_removed.rs | 38 ++ .../tests/inputs/pass/removed.rs | 49 ++ ...__pass@conversion_tracking_removed.rs.snap | 626 ++++++++++++++++++ ...ed_macros__snapshots__pass@removed.rs.snap | 131 ++++ .../tests/trybuild.rs | 3 + crates/stackable-versioned/CHANGELOG.md | 8 + 16 files changed, 1192 insertions(+), 16 deletions(-) create mode 100644 crates/stackable-versioned-macros/tests/inputs/fail/removed.rs create mode 100644 crates/stackable-versioned-macros/tests/inputs/fail/removed.stderr create mode 100644 crates/stackable-versioned-macros/tests/inputs/pass/conversion_tracking_removed.rs create mode 100644 crates/stackable-versioned-macros/tests/inputs/pass/removed.rs create mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@conversion_tracking_removed.rs.snap create mode 100644 crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@removed.rs.snap diff --git a/crates/stackable-versioned-macros/src/attrs/item/mod.rs b/crates/stackable-versioned-macros/src/attrs/item/mod.rs index 568f318d7..768f8a57c 100644 --- a/crates/stackable-versioned-macros/src/attrs/item/mod.rs +++ b/crates/stackable-versioned-macros/src/attrs/item/mod.rs @@ -29,6 +29,8 @@ pub use variant::*; /// [`Vec`]. /// - An item can only be deprecated once. A field or variant not marked as /// 'deprecated' will be included up until the latest version. +/// - An item can only be removed once. It is not included in the version it is +/// removed in and all later versions. Removal must be the last action. #[derive(Debug, FromMeta)] pub struct CommonItemAttributes { /// This parses the `added` attribute on items (fields or variants). It can @@ -43,6 +45,10 @@ pub struct CommonItemAttributes { /// This parses the `deprecated` attribute on items (fields or variants). It /// can only be present at most once. pub deprecated: Option, + + /// This parses the `removed` attribute on items (fields or variants). It + /// can only be present at most once. + pub removed: Option, } // This impl block ONLY contains validation. The main entrypoint is the associated 'validate' @@ -90,6 +96,25 @@ impl CommonItemAttributes { ).with_span(&deprecated.version.span())); } + if let Some(removed) = &self.removed { + if !versions.iter().any(|v| v.inner == *removed.version) { + errors.push(Error::custom( + "the `removed` action uses a version which is not declared via `#[versioned(version)]`", + ).with_span(&removed.version.span())); + } + + // An item removed in the first version would never exist. + if versions + .first() + .is_some_and(|v| v.inner == *removed.version) + { + errors.push( + Error::custom("cannot be marked as `removed` in the first version") + .with_span(&removed.version.span()), + ); + } + } + errors.finish() } @@ -106,7 +131,29 @@ impl CommonItemAttributes { /// at least one version before being changed. /// - `changed` and `deprecated` using the same version: Again, the same /// rules from above apply here as well. + /// - `removed` and any other action using the same version: Again, the + /// same rules from above apply here as well. fn validate_action_combinations(&self, item_idents: &impl ItemIdents) -> Result<()> { + if let Some(removed) = &self.removed { + let removed_version = *removed.version; + + if self + .added + .as_ref() + .is_some_and(|a| *a.version == removed_version) + || self.changes.iter().any(|c| *c.version == removed_version) + || self + .deprecated + .as_ref() + .is_some_and(|d| *d.version == removed_version) + { + return Err(Error::custom( + "cannot be marked as `removed` and any other action in the same version", + ) + .with_span(item_idents.original())); + } + } + match (&self.added, &self.changes, &self.deprecated) { (Some(added), _, Some(deprecated)) if *added.version == *deprecated.version => Err( Error::custom("cannot be marked as `added` and `deprecated` in the same version") @@ -142,6 +189,7 @@ impl CommonItemAttributes { /// version of the added action. /// - All `changed` actions must use a greater version than `added` but a /// lesser version than `deprecated`. + /// - `removed` must use a greater version than all other actions. fn validate_action_order(&self, item_idents: &impl ItemIdents) -> Result<()> { let added_version = self.added.as_ref().map(|a| *a.version); let deprecated_version = self.deprecated.as_ref().map(|d| *d.version); @@ -169,6 +217,21 @@ impl CommonItemAttributes { .with_span(item_idents.original())); } + // Lastly, ensure that the item is removed after all other actions. + if let Some(removed) = &self.removed { + let removed_version = *removed.version; + + if added_version.is_some_and(|a| a > removed_version) + || deprecated_version.is_some_and(|d| d > removed_version) + || self.changes.iter().any(|c| *c.version > removed_version) + { + return Err(Error::custom( + "`removed` must use a version higher than all other actions", + ) + .with_span(item_idents.original())); + } + } + Ok(()) } @@ -317,8 +380,32 @@ impl CommonItemAttributes { .collect() } - #[expect(clippy::too_many_lines)] pub fn into_changeset( + mut self, + idents: &impl ItemIdents, + ty: Type, + ) -> Option> { + let removed = self.removed.take(); + let mut actions = self.into_changeset_without_removal(idents, ty); + + // The removal is always the last action (which is ensured during validation). The item uses + // the original ident in the last version it is present in, regardless if it was deprecated + // or not. + if let Some(removed) = removed { + actions.get_or_insert_default().insert( + *removed.version, + ItemStatus::Removal { + default_fn: removed.default_fn.deref().clone(), + ident: idents.original().clone(), + }, + ); + } + + actions + } + + #[expect(clippy::too_many_lines)] + fn into_changeset_without_removal( self, idents: &impl ItemIdents, ty: Type, @@ -513,3 +600,17 @@ pub struct DeprecatedAttributes { pub note: Option>, pub previous_docs: Option>, } + +/// For the removed() action +/// +/// Example usage: +/// - `removed(in = "...")` +/// - `removed(in = "...", default = "custom_fn")` +#[derive(Clone, Debug, FromMeta)] +pub struct RemovedAttributes { + #[darling(rename = "in")] + pub version: SpannedValue, + + #[darling(rename = "default", default = "default_default_fn")] + pub default_fn: SpannedValue, +} diff --git a/crates/stackable-versioned-macros/src/attrs/item/variant.rs b/crates/stackable-versioned-macros/src/attrs/item/variant.rs index 4fe85072b..41f02ecab 100644 --- a/crates/stackable-versioned-macros/src/attrs/item/variant.rs +++ b/crates/stackable-versioned-macros/src/attrs/item/variant.rs @@ -60,6 +60,15 @@ impl VariantAttributes { errors.handle(self.common.validate(&variant_idents, &self.attrs)); + // Removing a variant would require a variant to map to during upgrades, which is currently + // not supported. + if let Some(removed) = &self.common.removed { + errors.push( + Error::custom("the `removed` action is currently not supported on enum variants") + .with_span(&removed.version.span()), + ); + } + // Validate names of renames for change in &self.common.changes { if let Some(previous_name) = &change.previous_name diff --git a/crates/stackable-versioned-macros/src/codegen/changes.rs b/crates/stackable-versioned-macros/src/codegen/changes.rs index 3e1355a4f..cd3e53512 100644 --- a/crates/stackable-versioned-macros/src/codegen/changes.rs +++ b/crates/stackable-versioned-macros/src/codegen/changes.rs @@ -132,6 +132,16 @@ impl ChangesetExt for BTreeMap { ty: Box::new(ty.clone()), }, ), + // If the removal is the only action, the item is present with its original + // ident and type in all versions before. + ItemStatus::Removal { ident, .. } => self.insert( + version.inner, + ItemStatus::NoChange { + previously_deprecated: false, + ident: ident.clone(), + ty: Box::new(ty.clone()), + }, + ), ItemStatus::NoChange { previously_deprecated, ident, @@ -163,7 +173,11 @@ impl ChangesetExt for BTreeMap { ty, .. } => (ident, ty, *previously_deprecated), - ItemStatus::NotPresent => unreachable!(), + // The item is not present in any version after it has been removed. + ItemStatus::Removal { .. } | ItemStatus::NotPresent => { + self.insert(version.inner, ItemStatus::NotPresent); + continue; + } }; self.insert( @@ -189,7 +203,9 @@ impl ChangesetExt for BTreeMap { ty, .. } => (ident, ty, *previously_deprecated), - ItemStatus::NotPresent => { + // NOTE: The removal is always the last action, so it can't have an upper + // neighbor. It is still handled here for completeness. + ItemStatus::Removal { .. } | ItemStatus::NotPresent => { self.insert(version.inner, ItemStatus::NotPresent); continue; } 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 02bf77a0f..4ff9cdc79 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs @@ -389,6 +389,7 @@ impl Struct { ItemStatus::Change { .. } => s.is_type_change(), ItemStatus::Deprecation { .. } | ItemStatus::NoChange { .. } + | ItemStatus::Removal { .. } | ItemStatus::NotPresent => false, } }) diff --git a/crates/stackable-versioned-macros/src/codegen/item/field.rs b/crates/stackable-versioned-macros/src/codegen/item/field.rs index 08102e13b..da0e11cc0 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/field.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/field.rs @@ -156,7 +156,7 @@ impl VersionedField { pub #field_ident: #field_type, }) } - ItemStatus::NotPresent => None, + ItemStatus::Removal { .. } | ItemStatus::NotPresent => None, ItemStatus::NoChange { previously_deprecated, ident, @@ -225,8 +225,11 @@ impl VersionedField { 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) => None, + // the From impl. The same applies to all versions after a field was removed. + ( + ItemStatus::NotPresent | ItemStatus::Removal { .. }, + ItemStatus::NotPresent, + ) => None, ( _, ItemStatus::Addition { @@ -236,6 +239,19 @@ impl VersionedField { Direction::Upgrade => Some(quote! { #ident: #default_fn(), }), Direction::Downgrade => None, }, + // The field doesn't exist in the next version anymore. As such, it is dropped + // during upgrades and needs to be initialized during downgrades. + // TODO: Track the value of removed fields when experimental_conversion_tracking + // is enabled. Currently, the value is lost during the upgrade. + ( + _, + ItemStatus::Removal { + ident, default_fn, .. + }, + ) => match direction { + Direction::Upgrade => None, + Direction::Downgrade => Some(quote! { #ident: #default_fn(), }), + }, ( _, ItemStatus::Change { @@ -307,8 +323,9 @@ impl VersionedField { let changes = self.changes.as_ref()?; match direction { - // This arm is only relevant for removed fields which are currently - // not supported. + // This arm is only relevant for removed fields, which are currently + // not tracked. + // TODO: Track the value of removed fields. Direction::Upgrade => None, // When we generate code for a downgrade, any changes which need to diff --git a/crates/stackable-versioned-macros/src/codegen/item/mod.rs b/crates/stackable-versioned-macros/src/codegen/item/mod.rs index 8b5ecde8d..ebbc0ab24 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/mod.rs @@ -66,6 +66,11 @@ pub enum ItemStatus { ident: IdentString, ty: Box, }, + Removal { + /// The ident of the item in the last version it is present in. + ident: IdentString, + default_fn: Path, + }, NotPresent, } @@ -79,6 +84,7 @@ impl ItemStatus { } | Self::Deprecation { ident, .. } | Self::NoChange { ident, .. } => ident, + Self::Removal { .. } => unreachable!("ItemStatus::Removal does not have an ident"), Self::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 22bf43da0..d2348b347 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/variant.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/variant.rs @@ -139,7 +139,8 @@ impl VersionedVariant { #ident #fields, }) } - ItemStatus::NotPresent => None, + // Removing variants is rejected during validation. + ItemStatus::Removal { .. } | ItemStatus::NotPresent => None, }, None => { // If there is no chain of variant actions, the variant is not diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs index 0df530807..0b6d6af2b 100644 --- a/crates/stackable-versioned-macros/src/lib.rs +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -377,9 +377,9 @@ mod utils; /// /// ## Field Actions /// -/// This crate currently supports three different item actions. Items can -/// be added, changed, and deprecated. The macro ensures that these actions -/// adhere to the following set of rules: +/// This crate currently supports four different item actions. Items can +/// be added, changed, deprecated, and removed. The macro ensures that these +/// actions adhere to the following set of rules: /// /// 1. Items cannot be added and deprecated in the same version. /// 2. Items cannot be added and changed in the same version. @@ -389,13 +389,17 @@ mod utils; /// version _c_ must ensure _a < b1, ..., bn < c_. /// 5. All item actions must use previously declared versions. Using versions /// not present at the container level will result in an error. +/// 6. Items cannot be removed in the same version as any other action. +/// 7. Items removed in version _d_ must ensure that _d_ is greater than the +/// versions of all other actions. Items cannot be removed in the first +/// version. /// /// For items marked as deprecated, one additional rule applies: /// /// - Fields must start with the `deprecated_` and variants with the -/// `Deprecated` prefix. This is enforced because Kubernetes doesn't allow -/// removing fields in CRDs entirely. Instead, they should be marked as -/// deprecated. By convention this is done with the `deprecated` prefix. +/// `Deprecated` prefix. Deprecated items continue to exist in all later +/// versions. By convention this is indicated with the `deprecated` prefix. +/// To drop a field from later versions, use the `removed` action. /// /// ### Added Action /// @@ -579,7 +583,8 @@ mod utils; /// ### Deprecated Action /// /// This action indicates that an item is deprecated in a particular version. -/// Deprecated items are not removed. Available arguments are: +/// Deprecated items are not removed, see the [removed action](#removed-action) +/// for that. Available arguments are: /// /// - `in` to indicate in which version the item is deprecated. /// - `note` to specify an optional deprecation note. @@ -638,6 +643,107 @@ mod utils; /// ``` /// /// +/// ### Removed Action +/// +/// This action indicates that a field is removed in a particular version. The +/// field is not part of the container in that version and all later versions. +/// The field can, but doesn't need to be deprecated in an earlier version. +/// Removing enum variants is currently not supported. Available arguments are: +/// +/// - `in` to indicate in which version the field is removed. +/// - `default` to customize the function used to initialize the field when +/// downgrading to the last version the field is present in. Uses +/// [`Default::default`] if not provided. +/// +///
+/// +/// The value of a removed field is lost when upgrading to the version it is +/// removed in. Removed fields are currently not tracked by the +/// [conversion tracking](#conversion-tracking) mechanism. +/// +///
+/// +/// ``` +/// # use stackable_versioned_macros::versioned; +/// #[versioned( +/// version(name = "v1alpha1"), +/// version(name = "v1beta1"), +/// version(name = "v1") +/// )] +/// mod versioned { +/// pub struct Foo { +/// #[versioned( +/// deprecated(in = "v1beta1"), +/// removed(in = "v1", default = "default_bar") +/// )] +/// deprecated_bar: usize, +/// baz: bool, +/// } +/// } +/// +/// fn default_bar() -> usize { +/// 42 +/// } +/// ``` +/// +///
+/// Expand Generated Code +/// +/// 1. In version `v1alpha1` the field `bar` is neither deprecated nor removed. +/// 2. In version `v1beta1` the field is deprecated and includes the +/// `deprecated_` prefix. +/// 3. In version `v1` the field is removed. Its value is dropped during the +/// upgrade. +/// 4. When downgrading from `v1`, the field is initialized using the provided +/// `default_bar()` function. +/// +/// ```ignore +/// pub mod v1alpha1 { +/// use super::*; +/// pub struct Foo { +/// pub bar: usize, // 1 +/// pub baz: bool, +/// } +/// } +/// +/// // Snip +/// +/// pub mod v1beta1 { +/// use super::*; +/// pub struct Foo { +/// #[deprecated] // 2 +/// pub deprecated_bar: usize, +/// pub baz: bool, +/// } +/// } +/// +/// pub mod v1 { +/// use super::*; +/// pub struct Foo { +/// pub baz: bool, // 3 +/// } +/// } +/// +/// impl From for v1::Foo { +/// fn from(foo: v1beta1::Foo) -> Self { +/// Self { +/// baz: foo.baz, // 3 +/// } +/// } +/// } +/// +/// #[allow(deprecated)] +/// impl From for v1beta1::Foo { +/// fn from(foo: v1::Foo) -> Self { +/// Self { +/// deprecated_bar: default_bar(), // 4 +/// baz: foo.baz, +/// } +/// } +/// } +/// ``` +///
+/// /// ### Changed Docs /// /// The doc comments of an item can be changed using the `previous_docs` argument diff --git a/crates/stackable-versioned-macros/tests/inputs/fail/removed.rs b/crates/stackable-versioned-macros/tests/inputs/fail/removed.rs new file mode 100644 index 000000000..de04d9b04 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/fail/removed.rs @@ -0,0 +1,35 @@ +use stackable_versioned_macros::versioned; + +#[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + version(name = "v1") +)] +mod versioned { + struct RemovedInFirstVersion { + #[versioned(removed(in = "v1alpha1"))] + foo: usize, + } + + struct RemovedInUnknownVersion { + #[versioned(removed(in = "v2"))] + foo: usize, + } + + struct RemovedAndDeprecatedInSameVersion { + #[versioned(deprecated(in = "v1"), removed(in = "v1"))] + deprecated_foo: usize, + } + + struct RemovedBeforeChange { + #[versioned(changed(in = "v1", previous_name = "bar"), removed(in = "v1beta1"))] + foo: usize, + } + + enum RemovedVariant { + #[versioned(removed(in = "v1"))] + Foo, + } +} + +fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/fail/removed.stderr b/crates/stackable-versioned-macros/tests/inputs/fail/removed.stderr new file mode 100644 index 000000000..86b6970e1 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/fail/removed.stderr @@ -0,0 +1,29 @@ +error: cannot be marked as `removed` in the first version + --> tests/inputs/fail/removed.rs:10:34 + | +10 | #[versioned(removed(in = "v1alpha1"))] + | ^^^^^^^^^^ + +error: the `removed` action uses a version which is not declared via `#[versioned(version)]` + --> tests/inputs/fail/removed.rs:15:34 + | +15 | #[versioned(removed(in = "v2"))] + | ^^^^ + +error: cannot be marked as `removed` and any other action in the same version + --> tests/inputs/fail/removed.rs:21:9 + | +21 | deprecated_foo: usize, + | ^^^^^^^^^^^^^^ + +error: `removed` must use a version higher than all other actions + --> tests/inputs/fail/removed.rs:26:9 + | +26 | foo: usize, + | ^^^ + +error: the `removed` action is currently not supported on enum variants + --> tests/inputs/fail/removed.rs:30:34 + | +30 | #[versioned(removed(in = "v1"))] + | ^^^^ diff --git a/crates/stackable-versioned-macros/tests/inputs/pass/conversion_tracking_removed.rs b/crates/stackable-versioned-macros/tests/inputs/pass/conversion_tracking_removed.rs new file mode 100644 index 000000000..44b179feb --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/pass/conversion_tracking_removed.rs @@ -0,0 +1,38 @@ +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use stackable_versioned::versioned; +// --- +#[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + version(name = "v1"), + options(k8s(experimental_conversion_tracking)) +)] +// --- +pub(crate) mod versioned { + #[versioned(crd(group = "stackable.tech", doc = "Test"))] + #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] + pub(crate) struct FooSpec { + // Removed fields are currently not tracked. Their value is lost during upgrades. + #[versioned(removed(in = "v1beta1"))] + bar: usize, + + #[versioned(deprecated(in = "v1beta1"), removed(in = "v1"))] + deprecated_baz: bool, + + #[versioned(nested)] + quux: Quux, + } + + #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] + pub(crate) struct Quux { + corge: String, + + // An added field is tracked until it is removed. + #[versioned(added(in = "v1beta1"), removed(in = "v1"))] + grault: String, + } +} +// --- +fn main() {} diff --git a/crates/stackable-versioned-macros/tests/inputs/pass/removed.rs b/crates/stackable-versioned-macros/tests/inputs/pass/removed.rs new file mode 100644 index 000000000..2c40304ca --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/pass/removed.rs @@ -0,0 +1,49 @@ +use stackable_versioned_macros::versioned; +// --- +#[versioned( + version(name = "v1alpha1"), + version(name = "v1alpha2"), + version(name = "v1beta1"), + version(name = "v1"), + version(name = "v2") +)] +// --- +mod versioned { + struct Foo { + username: String, + + /// Removed without being deprecated first. + #[versioned(removed(in = "v1alpha2"))] + nickname: String, + + #[versioned( + deprecated(in = "v1alpha2"), + removed(in = "v1beta1", default = default_age) + )] + deprecated_age: u32, + + #[versioned( + changed( + in = "v1alpha2", + previous_name = "colour", + previous_type = "u8", + downgrade_with = u16_to_u8 + ), + removed(in = "v1") + )] + color: u16, + + #[versioned(added(in = "v1alpha2"), removed(in = "v1"))] + email: String, + } +} +// --- +fn main() {} + +fn default_age() -> u32 { + 42 +} + +fn u16_to_u8(input: u16) -> u8 { + input.try_into().unwrap() +} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@conversion_tracking_removed.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@conversion_tracking_removed.rs.snap new file mode 100644 index 000000000..56ab42611 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@conversion_tracking_removed.rs.snap @@ -0,0 +1,626 @@ +--- +source: crates/stackable-versioned-macros/src/lib.rs +expression: formatted +input_file: crates/stackable-versioned-macros/tests/inputs/pass/conversion_tracking_removed.rs +--- +#[automatically_derived] +pub(crate) mod v1alpha1 { + use super::*; + #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] + #[kube( + group = "stackable.tech", + version = "v1alpha1", + kind = "Foo", + doc = "Test", + status = FooStatusWithChangedValues + )] + pub struct FooSpec { + pub bar: usize, + pub baz: bool, + pub quux: Quux, + } + #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] + pub struct Quux { + pub corge: String, + } +} +#[automatically_derived] +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 { + metadata: __sv_foo.metadata, + status: Some(status), + spec, + } + } +} +#[automatically_derived] +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] +#[allow(deprecated)] +impl<__SvStatus> ::stackable_versioned::TrackingFrom +for v1beta1::FooSpec +where + __SvStatus: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from( + __sv_foospec: v1alpha1::FooSpec, + status: &mut __SvStatus, + parent: &str, + ) -> Self { + use ::stackable_versioned::{ExtractIfExt as _, TrackingInto as _}; + let __sv_quux_path = ::stackable_versioned::jthong_path(parent, "quux"); + let mut spec = Self { + deprecated_baz: __sv_foospec.baz.into(), + quux: __sv_foospec.quux.tracking_into(status, &__sv_quux_path), + }; + spec + } +} +#[automatically_derived] +#[allow(deprecated)] +impl<__SvStatus> ::stackable_versioned::TrackingFrom +for v1alpha1::FooSpec +where + __SvStatus: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from( + __sv_foospec: v1beta1::FooSpec, + status: &mut __SvStatus, + parent: &str, + ) -> Self { + use ::stackable_versioned::{ExtractIfExt as _, TrackingInto as _}; + let __sv_quux_path = ::stackable_versioned::jthong_path(parent, "quux"); + let mut spec = Self { + bar: ::std::default::Default::default(), + baz: __sv_foospec.deprecated_baz.into(), + quux: __sv_foospec.quux.tracking_into(status, &__sv_quux_path), + }; + spec + } +} +#[automatically_derived] +impl<__SvStatus> ::stackable_versioned::TrackingFrom +for v1beta1::Quux +where + __SvStatus: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from( + __sv_quux: v1alpha1::Quux, + status: &mut __SvStatus, + parent: &str, + ) -> Self { + use ::stackable_versioned::{ExtractIfExt as _, TrackingInto as _}; + let __sv_grault_path = ::stackable_versioned::jthong_path(parent, "grault"); + let mut spec = Self { + corge: __sv_quux.corge.into(), + grault: ::std::default::Default::default(), + }; + if let Some(upgrades) = status.changes().upgrades.get_mut("v1beta1") { + if let Some(::stackable_versioned::ChangedValue { value, .. }) = upgrades + .extract_first_if(|u| u.json_path == __sv_grault_path) + { + spec.grault = ::serde_json::from_value(value) + .expect( + "internal error: {__sv_grault_path} must be serializable for spec.grault", + ); + } + if upgrades.is_empty() { + status.changes().upgrades.remove("v1beta1"); + } + } + spec + } +} +#[automatically_derived] +impl<__SvStatus> ::stackable_versioned::TrackingFrom +for v1alpha1::Quux +where + __SvStatus: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from( + __sv_quux: v1beta1::Quux, + status: &mut __SvStatus, + parent: &str, + ) -> Self { + use ::stackable_versioned::{ExtractIfExt as _, TrackingInto as _}; + let __sv_grault_path = ::stackable_versioned::jthong_path(parent, "grault"); + let upgrades = status + .changes() + .upgrades + .entry("v1beta1".to_owned()) + .or_default(); + upgrades + .push(::stackable_versioned::ChangedValue { + json_path: __sv_grault_path, + value: ::serde_json::to_value(&__sv_quux.grault).unwrap(), + downgraded_value: ::core::option::Option::None, + }); + let mut spec = Self { + corge: __sv_quux.corge.into(), + }; + spec + } +} +#[automatically_derived] +pub(crate) mod v1beta1 { + use super::*; + #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] + #[kube( + group = "stackable.tech", + version = "v1beta1", + kind = "Foo", + doc = "Test", + status = FooStatusWithChangedValues + )] + pub struct FooSpec { + #[deprecated] + pub deprecated_baz: bool, + pub quux: Quux, + } + #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] + pub struct Quux { + pub corge: String, + pub grault: String, + } +} +#[automatically_derived] +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 { + metadata: __sv_foo.metadata, + status: Some(status), + spec, + } + } +} +#[automatically_derived] +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] +#[allow(deprecated)] +impl<__SvStatus> ::stackable_versioned::TrackingFrom +for v1::FooSpec +where + __SvStatus: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from( + __sv_foospec: v1beta1::FooSpec, + status: &mut __SvStatus, + parent: &str, + ) -> Self { + use ::stackable_versioned::{ExtractIfExt as _, TrackingInto as _}; + let __sv_quux_path = ::stackable_versioned::jthong_path(parent, "quux"); + let mut spec = Self { + quux: __sv_foospec.quux.tracking_into(status, &__sv_quux_path), + }; + spec + } +} +#[automatically_derived] +#[allow(deprecated)] +impl<__SvStatus> ::stackable_versioned::TrackingFrom +for v1beta1::FooSpec +where + __SvStatus: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from( + __sv_foospec: v1::FooSpec, + status: &mut __SvStatus, + parent: &str, + ) -> Self { + use ::stackable_versioned::{ExtractIfExt as _, TrackingInto as _}; + let __sv_quux_path = ::stackable_versioned::jthong_path(parent, "quux"); + let mut spec = Self { + deprecated_baz: ::std::default::Default::default(), + quux: __sv_foospec.quux.tracking_into(status, &__sv_quux_path), + }; + spec + } +} +#[automatically_derived] +impl<__SvStatus> ::stackable_versioned::TrackingFrom +for v1::Quux +where + __SvStatus: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from( + __sv_quux: v1beta1::Quux, + status: &mut __SvStatus, + parent: &str, + ) -> Self { + use ::stackable_versioned::{ExtractIfExt as _, TrackingInto as _}; + let mut spec = Self { + corge: __sv_quux.corge.into(), + }; + spec + } +} +#[automatically_derived] +impl<__SvStatus> ::stackable_versioned::TrackingFrom +for v1beta1::Quux +where + __SvStatus: ::stackable_versioned::TrackingStatus + ::core::default::Default, +{ + fn tracking_from( + __sv_quux: v1::Quux, + status: &mut __SvStatus, + parent: &str, + ) -> Self { + use ::stackable_versioned::{ExtractIfExt as _, TrackingInto as _}; + let mut spec = Self { + corge: __sv_quux.corge.into(), + grault: ::std::default::Default::default(), + }; + spec + } +} +#[automatically_derived] +pub(crate) mod v1 { + use super::*; + #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] + #[kube( + group = "stackable.tech", + version = "v1", + kind = "Foo", + doc = "Test", + status = FooStatusWithChangedValues + )] + pub struct FooSpec { + pub quux: Quux, + } + #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] + pub struct Quux { + pub corge: String, + } +} +#[automatically_derived] +#[derive(::core::fmt::Debug)] +pub(crate) enum Foo { + V1Alpha1(v1alpha1::Foo), + V1Beta1(v1beta1::Foo), + V1(v1::Foo), +} +#[automatically_derived] +impl Foo { + /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. + pub fn merged_crd( + stored_apiversion: FooVersion, + ) -> ::std::result::Result< + ::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, + ::kube::core::crd::MergeError, + > { + ::kube::core::crd::merge_crds( + vec![ + < v1alpha1::Foo as ::kube::core::CustomResourceExt > ::crd(), < + v1beta1::Foo as ::kube::core::CustomResourceExt > ::crd(), < v1::Foo as + ::kube::core::CustomResourceExt > ::crd() + ], + stored_apiversion.as_version_str(), + ) + } + /// Tries to convert a list of objects of kind [`Foo`] 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]: ::kube::core::conversion::ConversionReview + pub fn try_convert( + review: ::kube::core::conversion::ConversionReview, + ) -> ::kube::core::conversion::ConversionReview { + let request = match ::kube::core::conversion::ConversionRequest::from_review( + review, + ) { + ::std::result::Result::Ok(request) => request, + ::std::result::Result::Err(err) => { + return ::kube::core::conversion::ConversionResponse::invalid(::kube::core::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: err.to_string(), + metadata: None, + reason: err.to_string(), + details: None, + code: 400, + }) + .into_review(); + } + }; + let response = match Self::convert_objects( + request.objects, + &request.desired_api_version, + ) { + ::std::result::Result::Ok(converted_objects) => { + ::kube::core::conversion::ConversionResponse { + result: ::kube::core::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::conversion::ConversionResponse { + result: ::kube::core::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: message.clone(), + metadata: None, + reason: message, + details: None, + code, + }, + types: request.types, + uid: request.uid, + converted_objects: vec![], + } + } + }; + response.into_review() + } + fn convert_objects( + objects: ::std::vec::Vec<::serde_json::Value>, + desired_api_version: &str, + ) -> ::std::result::Result< + ::std::vec::Vec<::serde_json::Value>, + ::stackable_versioned::ConvertObjectError, + > { + let desired_api_version = FooVersion::from_api_version(desired_api_version) + .map_err(|source| ::stackable_versioned::ConvertObjectError::ParseDesiredApiVersion { + source, + })?; + let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); + for object in objects { + let current_object = Self::from_json_object(object.clone()) + .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { + source, + })?; + match (current_object, desired_api_version) { + (Self::V1Alpha1(__sv_foo), FooVersion::V1Beta1) => { + 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 { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Alpha1(__sv_foo), FooVersion::V1) => { + 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 { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Beta1(__sv_foo), FooVersion::V1Alpha1) => { + 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 { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Beta1(__sv_foo), FooVersion::V1) => { + 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 { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foo), FooVersion::V1Alpha1) => { + 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 { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foo), FooVersion::V1Beta1) => { + 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 { + source, + })?; + converted_objects.push(desired_object); + } + _ => converted_objects.push(object), + } + } + ::std::result::Result::Ok(converted_objects) + } + fn from_json_object( + object_value: ::serde_json::Value, + ) -> ::std::result::Result { + let kind = object_value + .get("kind") + .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 kind != "Foo" { + return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { + kind: kind.to_owned(), + expected: "Foo".to_owned(), + }); + } + let api_version = object_value + .get("apiVersion") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { + field: "apiVersion".to_owned(), + })? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "apiVersion".to_owned(), + })?; + let object = match api_version { + "stackable.tech/v1alpha1" => { + let object = ::serde_json::from_value(object_value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Alpha1(object) + } + "stackable.tech/v1beta1" => { + let object = ::serde_json::from_value(object_value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Beta1(object) + } + "stackable.tech/v1" => { + let object = ::serde_json::from_value(object_value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1(object) + } + unknown_api_version => { + return ::std::result::Result::Err(::stackable_versioned::ParseObjectError::UnknownApiVersion { + api_version: unknown_api_version.to_owned(), + }); + } + }; + ::std::result::Result::Ok(object) + } + fn into_json_value( + self, + ) -> ::std::result::Result<::serde_json::Value, ::serde_json::Error> { + match self { + Self::V1Alpha1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + Self::V1Beta1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + Self::V1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + } + } +} +#[automatically_derived] +#[derive(::core::marker::Copy, ::core::clone::Clone, ::core::fmt::Debug)] +pub(crate) enum FooVersion { + V1Alpha1, + V1Beta1, + V1, +} +#[automatically_derived] +impl ::core::fmt::Display for FooVersion { + fn fmt( + &self, + f: &mut ::core::fmt::Formatter<'_>, + ) -> ::std::result::Result<(), ::std::fmt::Error> { + f.write_str(self.as_version_str()) + } +} +#[automatically_derived] +impl FooVersion { + pub fn as_version_str(&self) -> &str { + match self { + FooVersion::V1Alpha1 => "v1alpha1", + FooVersion::V1Beta1 => "v1beta1", + FooVersion::V1 => "v1", + } + } + pub fn as_api_version_str(&self) -> &str { + match self { + FooVersion::V1Alpha1 => "stackable.tech/v1alpha1", + FooVersion::V1Beta1 => "stackable.tech/v1beta1", + FooVersion::V1 => "stackable.tech/v1", + } + } + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "stackable.tech/v1alpha1" => Ok(FooVersion::V1Alpha1), + "stackable.tech/v1beta1" => Ok(FooVersion::V1Beta1), + "stackable.tech/v1" => Ok(FooVersion::V1), + _ => { + Err(::stackable_versioned::UnknownDesiredApiVersionError { + api_version: api_version.to_owned(), + }) + } + } + } +} +#[cfg(test)] +#[test] +fn FooSpec_roundtrip_down_up() { + ::stackable_versioned::test_utils::test_roundtrip::< + v1::FooSpec, + >(stringify!(Foo), "stackable.tech/v1", "stackable.tech/v1alpha1", Foo::try_convert); +} +#[cfg(test)] +#[test] +fn FooSpec_roundtrip_up_down() { + ::stackable_versioned::test_utils::test_roundtrip::< + v1alpha1::FooSpec, + >(stringify!(Foo), "stackable.tech/v1alpha1", "stackable.tech/v1", Foo::try_convert); +} +#[automatically_derived] +#[derive( + ::core::clone::Clone, + ::core::default::Default, + ::core::fmt::Debug, + ::serde::Deserialize, + ::serde::Serialize, + ::schemars::JsonSchema +)] +#[serde(rename_all = "camelCase")] +pub struct FooStatusWithChangedValues { + pub changed_values: ::stackable_versioned::ChangedValues, +} +#[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__snapshots__pass@removed.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@removed.rs.snap new file mode 100644 index 000000000..095fc3c3f --- /dev/null +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@removed.rs.snap @@ -0,0 +1,131 @@ +--- +source: crates/stackable-versioned-macros/src/lib.rs +expression: formatted +input_file: crates/stackable-versioned-macros/tests/inputs/pass/removed.rs +--- +#[automatically_derived] +mod v1alpha1 { + use super::*; + pub struct Foo { + pub username: String, + /// Removed without being deprecated first. + pub nickname: String, + pub age: u32, + pub colour: u8, + } +} +#[automatically_derived] +#[allow(deprecated)] +impl ::core::convert::From for v1alpha2::Foo { + fn from(__sv_foo: v1alpha1::Foo) -> Self { + Self { + username: __sv_foo.username.into(), + deprecated_age: __sv_foo.age.into(), + color: __sv_foo.colour.into(), + email: ::std::default::Default::default(), + } + } +} +#[automatically_derived] +#[allow(deprecated)] +impl ::core::convert::From for v1alpha1::Foo { + fn from(__sv_foo: v1alpha2::Foo) -> Self { + Self { + username: __sv_foo.username.into(), + nickname: ::std::default::Default::default(), + age: __sv_foo.deprecated_age.into(), + colour: u16_to_u8(__sv_foo.color), + } + } +} +#[automatically_derived] +mod v1alpha2 { + use super::*; + pub struct Foo { + pub username: String, + #[deprecated] + pub deprecated_age: u32, + pub color: u16, + pub email: String, + } +} +#[automatically_derived] +#[allow(deprecated)] +impl ::core::convert::From for v1beta1::Foo { + fn from(__sv_foo: v1alpha2::Foo) -> Self { + Self { + username: __sv_foo.username.into(), + color: __sv_foo.color.into(), + email: __sv_foo.email.into(), + } + } +} +#[automatically_derived] +#[allow(deprecated)] +impl ::core::convert::From for v1alpha2::Foo { + fn from(__sv_foo: v1beta1::Foo) -> Self { + Self { + username: __sv_foo.username.into(), + deprecated_age: default_age(), + color: __sv_foo.color.into(), + email: __sv_foo.email.into(), + } + } +} +#[automatically_derived] +mod v1beta1 { + use super::*; + pub struct Foo { + pub username: String, + pub color: u16, + pub email: String, + } +} +#[automatically_derived] +impl ::core::convert::From for v1::Foo { + fn from(__sv_foo: v1beta1::Foo) -> Self { + Self { + username: __sv_foo.username.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1beta1::Foo { + fn from(__sv_foo: v1::Foo) -> Self { + Self { + username: __sv_foo.username.into(), + color: ::std::default::Default::default(), + email: ::std::default::Default::default(), + } + } +} +#[automatically_derived] +mod v1 { + use super::*; + pub struct Foo { + pub username: String, + } +} +#[automatically_derived] +impl ::core::convert::From for v2::Foo { + fn from(__sv_foo: v1::Foo) -> Self { + Self { + username: __sv_foo.username.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1::Foo { + fn from(__sv_foo: v2::Foo) -> Self { + Self { + username: __sv_foo.username.into(), + } + } +} +#[automatically_derived] +mod v2 { + use super::*; + pub struct Foo { + pub username: String, + } +} diff --git a/crates/stackable-versioned-macros/tests/trybuild.rs b/crates/stackable-versioned-macros/tests/trybuild.rs index 5140cc08a..9621685bb 100644 --- a/crates/stackable-versioned-macros/tests/trybuild.rs +++ b/crates/stackable-versioned-macros/tests/trybuild.rs @@ -24,6 +24,7 @@ mod inputs { // mod conversion_tracking_enum; // mod conversion_tracking_generics; // mod conversion_tracking; + // mod conversion_tracking_removed; // mod crate_overrides; // mod crate_overrides_only_kube; // mod crd_doc; @@ -35,6 +36,7 @@ mod inputs { // mod module; // mod module_preserve; // mod printcolumn; + // mod removed; // mod renamed_field; // mod renamed_kind; // mod scale; @@ -47,6 +49,7 @@ mod inputs { // mod applied_to_struct; // mod changed; // mod deprecate; + // mod removed; // mod spec_suffix; // mod unknown_version; // mod submodule_invalid_name; diff --git a/crates/stackable-versioned/CHANGELOG.md b/crates/stackable-versioned/CHANGELOG.md index 1e5b74beb..5f35c20d0 100644 --- a/crates/stackable-versioned/CHANGELOG.md +++ b/crates/stackable-versioned/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Add support for the `removed` field action, which removes a field in a particular version. + Removed fields are not tracked when `experimental_conversion_tracking` is enabled yet. + It doesn't support enum variants either for now ([#XXXX]). + +[#XXXX]: https://github.com/stackabletech/operator-rs/pull/XXXX + ## [0.14.0] - 2026-11-03 ### Added From 0d89cbf0e9e2931cb11fc87da1e38bd423200b5d Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Thu, 5 Nov 2026 12:42:08 +0100 Subject: [PATCH 2/6] changelog --- crates/stackable-versioned/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/stackable-versioned/CHANGELOG.md b/crates/stackable-versioned/CHANGELOG.md index 5f35c20d0..5b57df47f 100644 --- a/crates/stackable-versioned/CHANGELOG.md +++ b/crates/stackable-versioned/CHANGELOG.md @@ -8,9 +8,9 @@ All notable changes to this project will be documented in this file. - Add support for the `removed` field action, which removes a field in a particular version. Removed fields are not tracked when `experimental_conversion_tracking` is enabled yet. - It doesn't support enum variants either for now ([#XXXX]). + It doesn't support enum variants either for now ([#1324]). -[#XXXX]: https://github.com/stackabletech/operator-rs/pull/XXXX +[#1324]: https://github.com/stackabletech/operator-rs/pull/1324 ## [0.14.0] - 2026-11-03 From 1a8ed365406add4d55ee90d6b058f30922cbb86c Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Thu, 12 Nov 2026 14:09:54 +0100 Subject: [PATCH 3/6] Update crates/stackable-versioned-macros/src/codegen/item/mod.rs Co-authored-by: Techassi --- crates/stackable-versioned-macros/src/codegen/item/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/stackable-versioned-macros/src/codegen/item/mod.rs b/crates/stackable-versioned-macros/src/codegen/item/mod.rs index ebbc0ab24..64f25131f 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/mod.rs @@ -83,8 +83,8 @@ impl ItemStatus { .. } | Self::Deprecation { ident, .. } - | Self::NoChange { ident, .. } => ident, - Self::Removal { .. } => unreachable!("ItemStatus::Removal does not have an ident"), + | Self::NoChange { ident, .. } + | Self::Removal { ident, .. } => ident, Self::NotPresent => unreachable!("ItemStatus::NotPresent does not have an ident"), } } From 0db212be85f148d7cdd8a3dc53ec7e79d36dfd37 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Thu, 12 Nov 2026 14:14:47 +0100 Subject: [PATCH 4/6] docs: Rename field -> item --- crates/stackable-versioned-macros/src/lib.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs index 0b6d6af2b..358f63b03 100644 --- a/crates/stackable-versioned-macros/src/lib.rs +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -645,20 +645,20 @@ mod utils; /// /// ### Removed Action /// -/// This action indicates that a field is removed in a particular version. The -/// field is not part of the container in that version and all later versions. -/// The field can, but doesn't need to be deprecated in an earlier version. +/// This action indicates that an item is removed in a particular version. The +/// item is not part of the container in that version and all later versions. +/// The item can, but doesn't need to be deprecated in an earlier version. /// Removing enum variants is currently not supported. Available arguments are: /// -/// - `in` to indicate in which version the field is removed. -/// - `default` to customize the function used to initialize the field when -/// downgrading to the last version the field is present in. Uses +/// - `in` to indicate in which version the item is removed. +/// - `default` to customize the function used to initialize the item when +/// downgrading to the last version the item is present in. Uses /// [`Default::default`] if not provided. /// ///
/// -/// The value of a removed field is lost when upgrading to the version it is -/// removed in. Removed fields are currently not tracked by the +/// The value of a removed item is lost when upgrading to the version it is +/// removed in. Removed items are currently not tracked by the /// [conversion tracking](#conversion-tracking) mechanism. /// ///
From aa3e03afdbc54c7001c21cbec7f03ee542a53c2a Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Thu, 12 Nov 2026 14:35:02 +0100 Subject: [PATCH 5/6] fix: Use versioned span --- .../stackable-versioned-macros/src/attrs/item/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/stackable-versioned-macros/src/attrs/item/mod.rs b/crates/stackable-versioned-macros/src/attrs/item/mod.rs index 768f8a57c..d4784da80 100644 --- a/crates/stackable-versioned-macros/src/attrs/item/mod.rs +++ b/crates/stackable-versioned-macros/src/attrs/item/mod.rs @@ -219,16 +219,16 @@ impl CommonItemAttributes { // Lastly, ensure that the item is removed after all other actions. if let Some(removed) = &self.removed { - let removed_version = *removed.version; + let removed_version = removed.version; - if added_version.is_some_and(|a| a > removed_version) - || deprecated_version.is_some_and(|d| d > removed_version) - || self.changes.iter().any(|c| *c.version > removed_version) + if added_version.is_some_and(|a| a > *removed_version) + || deprecated_version.is_some_and(|d| d > *removed_version) + || self.changes.iter().any(|c| *c.version > *removed_version) { return Err(Error::custom( "`removed` must use a version higher than all other actions", ) - .with_span(item_idents.original())); + .with_span(&removed_version.span())); } } From 02fa64fea0de6de0d1e19eca7d8eee7143f41002 Mon Sep 17 00:00:00 2001 From: Sebastian Bernauer Date: Thu, 12 Nov 2026 14:41:09 +0100 Subject: [PATCH 6/6] test: Update assertion --- .../tests/inputs/fail/removed.stderr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/stackable-versioned-macros/tests/inputs/fail/removed.stderr b/crates/stackable-versioned-macros/tests/inputs/fail/removed.stderr index 86b6970e1..90f6ec5d1 100644 --- a/crates/stackable-versioned-macros/tests/inputs/fail/removed.stderr +++ b/crates/stackable-versioned-macros/tests/inputs/fail/removed.stderr @@ -17,10 +17,10 @@ error: cannot be marked as `removed` and any other action in the same version | ^^^^^^^^^^^^^^ error: `removed` must use a version higher than all other actions - --> tests/inputs/fail/removed.rs:26:9 + --> tests/inputs/fail/removed.rs:25:77 | -26 | foo: usize, - | ^^^ +25 | #[versioned(changed(in = "v1", previous_name = "bar"), removed(in = "v1beta1"))] + | ^^^^^^^^^ error: the `removed` action is currently not supported on enum variants --> tests/inputs/fail/removed.rs:30:34