diff --git a/crates/stackable-versioned-macros/src/attrs/item/mod.rs b/crates/stackable-versioned-macros/src/attrs/item/mod.rs index 4c8e2dfcf..fe63e28d3 100644 --- a/crates/stackable-versioned-macros/src/attrs/item/mod.rs +++ b/crates/stackable-versioned-macros/src/attrs/item/mod.rs @@ -8,7 +8,7 @@ use syn::{Attribute, Path, Type, spanned::Spanned}; use crate::{ codegen::{VersionDefinition, item::ItemStatus}, - utils::ItemIdents, + utils::{ItemIdents, doc_comments::DocComments as _}, }; mod field; @@ -223,9 +223,12 @@ impl CommonItemAttributes { let mut errors = Error::accumulator(); for change in &self.changes { - if change.from_name.is_none() && change.from_type.is_none() { + if change.from_name.is_none() + && change.from_type.is_none() + && change.from_docs.is_none() + { errors.push(Error::custom( - "both `from_name` and `from_type` are unset. Is this `changed()` action needed?" + "`from_name`, `from_type` and `from_docs` are unset. Is this `changed()` action needed?" ).with_span(&change.since.span())); } @@ -288,6 +291,18 @@ impl CommonItemAttributes { } impl CommonItemAttributes { + /// Returns the doc comments of the item before each change which provides `from_docs`, keyed + /// by the version of the change. + pub fn previous_docs(&self) -> BTreeMap> { + self.changes + .iter() + .filter_map(|change| { + let docs = change.from_docs.as_deref()?; + Some((*change.since, docs.as_str().into_doc_comments())) + }) + .collect() + } + #[expect(clippy::too_many_lines)] pub fn into_changeset( self, @@ -458,11 +473,13 @@ fn default_default_fn() -> SpannedValue { /// - `changed(since = "...", from_name = "...", from_type="...")` /// - `changed(since = "...", from_name = "...", from_type="...", upgrade_with = "...")` /// - `changed(since = "...", from_name = "...", from_type="...", downgrade_with = "...")` +/// - `changed(since = "...", from_docs = "...")` #[derive(Clone, Debug, FromMeta)] pub struct ChangedAttributes { pub since: SpannedValue, pub from_name: Option>, pub from_type: Option>, + pub from_docs: Option>, pub upgrade_with: Option>, pub downgrade_with: Option>, } diff --git a/crates/stackable-versioned-macros/src/codegen/item/field.rs b/crates/stackable-versioned-macros/src/codegen/item/field.rs index ea074aeea..ecc81155e 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/field.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/field.rs @@ -11,7 +11,7 @@ use crate::{ codegen::{ Direction, VersionDefinition, changes::{BTreeMapExt, ChangesetExt}, - item::ItemStatus, + item::{ItemStatus, generate_attributes}, module::ModuleGenerationContext, }, utils::{ItemIdentExt, ItemIdents}, @@ -20,6 +20,7 @@ use crate::{ #[derive(Debug)] pub struct VersionedField { pub original_attributes: Vec, + pub previous_docs: BTreeMap>, pub changes: Option>, pub idents: FieldIdents, pub hint: Option, @@ -45,6 +46,7 @@ impl VersionedField { })?; let idents = FieldIdents::from(ident); + let previous_docs = field_attributes.common.previous_docs(); let changes = field_attributes .common .into_changeset(&idents, field.ty.clone()); @@ -53,6 +55,7 @@ impl VersionedField { Ok(Self { original_attributes: field_attributes.attrs, hint: field_attributes.hint, + previous_docs, ty: field.ty, changes, idents, @@ -82,7 +85,11 @@ impl VersionedField { /// } /// ``` pub fn generate_for_container(&self, version: &VersionDefinition) -> Option { - let original_attributes = &self.original_attributes; + let attributes = generate_attributes( + &self.original_attributes, + &self.previous_docs, + &version.inner, + ); #[allow(clippy::single_match_else)] match &self.changes { @@ -106,13 +113,13 @@ impl VersionedField { ) }) { ItemStatus::Addition { ident, ty, .. } => Some(quote! { - #(#original_attributes)* + #attributes pub #ident: #ty, }), ItemStatus::Change { to_ident, to_type, .. } => Some(quote! { - #(#original_attributes)* + #attributes pub #to_ident: #to_type, }), ItemStatus::Deprecation { @@ -133,7 +140,7 @@ impl VersionedField { }; Some(quote! { - #(#original_attributes)* + #attributes #deprecated_attr pub #field_ident: #field_type, }) @@ -149,7 +156,7 @@ impl VersionedField { let deprecated_attr = previously_deprecated.then(|| quote! {#[deprecated]}); Some(quote! { - #(#original_attributes)* + #attributes #deprecated_attr pub #ident: #ty, }) @@ -163,7 +170,7 @@ impl VersionedField { let field_type = &self.ty; Some(quote! { - #(#original_attributes)* + #attributes pub #field_ident: #field_type, }) } diff --git a/crates/stackable-versioned-macros/src/codegen/item/mod.rs b/crates/stackable-versioned-macros/src/codegen/item/mod.rs index 46abd7df8..f26b17747 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/mod.rs @@ -1,5 +1,12 @@ +use std::{collections::BTreeMap, ops::Bound}; + use darling::util::IdentString; -use syn::{Path, Type}; +use k8s_version::Version; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Attribute, Meta, Path, Type}; + +use crate::codegen::changes::Neighbors as _; mod field; pub use field::*; @@ -7,6 +14,31 @@ pub use field::*; mod variant; pub use variant::*; +/// Generates the attributes of an item (field or variant) for the provided `version`. +/// +/// If the docs of the item are changed in a later version (via `from_docs`), the original doc +/// comments are replaced by the docs which are valid in `version`. +pub fn generate_attributes( + original_attributes: &[Attribute], + previous_docs: &BTreeMap>, + version: &Version, +) -> TokenStream { + // The docs valid in this version are the previous docs of the closest change after this + // version. If there is no such change, the original docs are still valid. + let Some((_, docs)) = previous_docs.up_bound(Bound::Excluded(version)) else { + return quote! { #(#original_attributes)* }; + }; + + let attributes = original_attributes.iter().filter(|attribute| { + !matches!(&attribute.meta, Meta::NameValue(name_value) if name_value.path.is_ident("doc")) + }); + + quote! { + #(#[doc = #docs])* + #(#attributes)* + } +} + #[derive(Debug, PartialEq, Eq)] pub enum ItemStatus { Addition { diff --git a/crates/stackable-versioned-macros/src/codegen/item/variant.rs b/crates/stackable-versioned-macros/src/codegen/item/variant.rs index 0175b43aa..6455ef6b7 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/variant.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/variant.rs @@ -13,13 +13,14 @@ use crate::{ codegen::{ Direction, VersionDefinition, changes::{BTreeMapExt, ChangesetExt}, - item::ItemStatus, + item::{ItemStatus, generate_attributes}, }, utils::ItemIdents, }; pub struct VersionedVariant { pub original_attributes: Vec, + pub previous_docs: BTreeMap>, pub changes: Option>, pub idents: VariantIdents, pub fields: Fields, @@ -39,10 +40,12 @@ impl VersionedVariant { attrs: Vec::new(), bang_token: Not([Span::call_site()]), }); + let previous_docs = variant_attributes.common.previous_docs(); let changes = variant_attributes.common.into_changeset(&idents, ty); Ok(Self { original_attributes: variant_attributes.attrs, + previous_docs, fields: variant.fields, idents, changes, @@ -63,7 +66,11 @@ impl VersionedVariant { /// Generates tokens to be used in a container definition. pub fn generate_for_container(&self, version: &VersionDefinition) -> Option { - let original_attributes = &self.original_attributes; + let attributes = generate_attributes( + &self.original_attributes, + &self.previous_docs, + &version.inner, + ); let fields = &self.fields; #[allow(clippy::single_match_else)] @@ -80,11 +87,11 @@ impl VersionedVariant { ) }) { ItemStatus::Addition { ident, .. } => Some(quote! { - #(#original_attributes)* + #attributes #ident #fields, }), ItemStatus::Change { to_ident, .. } => Some(quote! { - #(#original_attributes)* + #attributes #to_ident #fields, }), ItemStatus::Deprecation { ident, note, .. } => { @@ -103,7 +110,7 @@ impl VersionedVariant { }; Some(quote! { - #(#original_attributes)* + #attributes #deprecated_attr #ident #fields, }) @@ -118,7 +125,7 @@ impl VersionedVariant { let deprecated_attr = previously_deprecated.then(|| quote! {#[deprecated]}); Some(quote! { - #(#original_attributes)* + #attributes #deprecated_attr #ident #fields, }) @@ -132,7 +139,7 @@ impl VersionedVariant { let ident = &self.idents.original; Some(quote! { - #(#original_attributes)* + #attributes #ident #fields, }) } diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs index 4e666a6b2..9859ea734 100644 --- a/crates/stackable-versioned-macros/src/lib.rs +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -496,6 +496,7 @@ mod utils; /// - `since` to indicate since which version the item is changed. /// - `from_name` to indicate from which previous name the field is renamed. /// - `from_type` to indicate from which previous type the field is changed. +/// - `from_docs` to provide the previous doc comments of the item. /// - `upgrade_with` to provide a custom upgrade function. This argument can /// only be used in combination with the `from_type` argument. The expected /// function signature is: `fn (OLD_TYPE) -> NEW_TYPE`. This function must @@ -561,6 +562,59 @@ mod utils; /// ``` /// /// +/// #### Changed Docs +/// +/// The doc comments of an item can be changed using the `from_docs` argument. +/// The doc comments attached to the item are used since the version of the +/// change. All earlier versions use the doc comments provided via `from_docs` +/// instead. This is especially useful to adjust the description of a field in +/// the generated CRD schema without changing it for older versions. The +/// argument can be used on its own or in combination with any other argument. +/// +/// ``` +/// # use stackable_versioned_macros::versioned; +/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))] +/// mod versioned { +/// pub struct Foo { +/// /// The number of bars. +/// #[versioned(changed(since = "v1beta1", from_docs = "The bar."))] +/// bar: usize, +/// baz: bool, +/// } +/// } +/// ``` +/// +///
+/// Expand Generated Code +/// +/// 1. In version `v1alpha1` the field uses the doc comments provided via +/// `from_docs`. +/// 2. In the next version, `v1beta1`, the field uses the doc comments attached +/// to the field. +/// +/// ```ignore +/// pub mod v1alpha1 { +/// use super::*; +/// pub struct Foo { +/// /// The bar. // 1 +/// pub bar: usize, +/// pub baz: bool, +/// } +/// } +/// +/// // Snip +/// +/// pub mod v1beta1 { +/// use super::*; +/// pub struct Foo { +/// /// The number of bars. // 2 +/// pub bar: usize, +/// pub baz: bool, +/// } +/// } +/// ``` +///
+/// /// ### Deprecated Action /// /// This action indicates that an item is deprecated in a particular version. diff --git a/crates/stackable-versioned-macros/tests/inputs/fail/changed.stderr b/crates/stackable-versioned-macros/tests/inputs/fail/changed.stderr index 61d10138f..417ca1250 100644 --- a/crates/stackable-versioned-macros/tests/inputs/fail/changed.stderr +++ b/crates/stackable-versioned-macros/tests/inputs/fail/changed.stderr @@ -10,7 +10,7 @@ error: the previous name must not start with the deprecation prefix 13 | changed(since = "v1", from_name = "deprecated_baz"), | ^^^^^^^^^^^^^^^^ -error: both `from_name` and `from_type` are unset. Is this `changed()` action needed? +error: `from_name`, `from_type` and `from_docs` are unset. Is this `changed()` action needed? --> tests/inputs/fail/changed.rs:14:29 | 14 | changed(since = "v2") diff --git a/crates/stackable-versioned-macros/tests/inputs/pass/docs.rs b/crates/stackable-versioned-macros/tests/inputs/pass/docs.rs index 23888ab90..c00fd6d5b 100644 --- a/crates/stackable-versioned-macros/tests/inputs/pass/docs.rs +++ b/crates/stackable-versioned-macros/tests/inputs/pass/docs.rs @@ -35,6 +35,37 @@ mod versioned { #[versioned(changed(since = "v1beta1", from_name = "qoox"))] #[versioned(changed(since = "v1", from_name = "qaax"))] quux: String, + + /// The docs of this field changed in v1beta1 and v2. + #[versioned( + changed(since = "v1beta1", from_docs = "These are the docs in v1alpha1."), + changed( + since = "v2", + from_docs = r#" + These are the docs from v1beta1 until v1. + + Multi-line docs are also supported. + "# + ) + )] + #[doc(alias = "grault")] + corge: String, + + /// The docs of this field changed in v1, while it was renamed in v1beta1. + #[versioned( + changed(since = "v1beta1", from_name = "waldo"), + changed(since = "v1", from_docs = "These are the docs before v1.") + )] + fred: String, + } + + /// Test + #[derive(Default)] + enum Bar { + /// The docs of this variant changed in v1. + #[versioned(changed(since = "v1", from_docs = "These are the docs before v1."))] + #[default] + Baz, } } // --- 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 index d37c68374..684338443 100644 --- 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 @@ -15,6 +15,18 @@ mod v1alpha1 { pub bar: String, /// This is will keep changing over time. pub qoox: String, + ///These are the docs in v1alpha1. + #[doc(alias = "grault")] + pub corge: String, + ///These are the docs before v1. + pub waldo: String, + } + /// Test + #[derive(Default)] + pub enum Bar { + ///These are the docs before v1. + #[default] + Baz, } } #[automatically_derived] @@ -31,6 +43,8 @@ where deprecated_bar: __sv_foo.bar.into(), baz: ::std::default::Default::default(), qaax: __sv_foo.qoox.into(), + corge: __sv_foo.corge.into(), + fred: __sv_foo.waldo.into(), }; if let Some(upgrades) = status.changes().upgrades.remove(&"v1beta1".to_owned()) { for ::stackable_versioned::ChangedValue { json_path, value } in upgrades { @@ -68,11 +82,29 @@ where foo: __sv_foo.foo.into(), bar: __sv_foo.deprecated_bar.into(), qoox: __sv_foo.qaax.into(), + corge: __sv_foo.corge.into(), + waldo: __sv_foo.fred.into(), }; spec } } #[automatically_derived] +impl ::core::convert::From for v1beta1::Bar { + fn from(__sv_bar: v1alpha1::Bar) -> Self { + match __sv_bar { + v1alpha1::Bar::Baz => v1beta1::Bar::Baz, + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1alpha1::Bar { + fn from(__sv_bar: v1beta1::Bar) -> Self { + match __sv_bar { + v1beta1::Bar::Baz => v1alpha1::Bar::Baz, + } + } +} +#[automatically_derived] mod v1beta1 { use super::*; ///Additional docs for this version which are purposefully long to @@ -90,6 +122,23 @@ mod v1beta1 { pub baz: String, /// This is will keep changing over time. pub qaax: String, + ///These are the docs from v1beta1 until v1. + /// + ///Multi-line docs are also supported. + #[doc(alias = "grault")] + pub corge: String, + ///These are the docs before v1. + pub fred: String, + } + ///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 enum Bar { + ///These are the docs before v1. + #[default] + Baz, } } #[automatically_derived] @@ -105,6 +154,8 @@ where deprecated_bar: __sv_foo.deprecated_bar.into(), baz: __sv_foo.baz.into(), qaax: __sv_foo.qaax.into(), + corge: __sv_foo.corge.into(), + fred: __sv_foo.fred.into(), }; spec } @@ -122,11 +173,29 @@ where deprecated_bar: __sv_foo.deprecated_bar.into(), baz: __sv_foo.baz.into(), qaax: __sv_foo.qaax.into(), + corge: __sv_foo.corge.into(), + fred: __sv_foo.fred.into(), }; spec } } #[automatically_derived] +impl ::core::convert::From for v1beta2::Bar { + fn from(__sv_bar: v1beta1::Bar) -> Self { + match __sv_bar { + v1beta1::Bar::Baz => v1beta2::Bar::Baz, + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1beta1::Bar { + fn from(__sv_bar: v1beta2::Bar) -> Self { + match __sv_bar { + v1beta2::Bar::Baz => v1beta1::Bar::Baz, + } + } +} +#[automatically_derived] mod v1beta2 { use super::*; /// Test @@ -141,6 +210,20 @@ mod v1beta2 { pub baz: String, /// This is will keep changing over time. pub qaax: String, + ///These are the docs from v1beta1 until v1. + /// + ///Multi-line docs are also supported. + #[doc(alias = "grault")] + pub corge: String, + ///These are the docs before v1. + pub fred: String, + } + /// Test + #[derive(Default)] + pub enum Bar { + ///These are the docs before v1. + #[default] + Baz, } } #[automatically_derived] @@ -156,6 +239,8 @@ where deprecated_bar: __sv_foo.deprecated_bar.into(), baz: __sv_foo.baz.into(), quux: __sv_foo.qaax.into(), + corge: __sv_foo.corge.into(), + fred: __sv_foo.fred.into(), }; spec } @@ -173,11 +258,29 @@ where deprecated_bar: __sv_foo.deprecated_bar.into(), baz: __sv_foo.baz.into(), qaax: __sv_foo.quux.into(), + corge: __sv_foo.corge.into(), + fred: __sv_foo.fred.into(), }; spec } } #[automatically_derived] +impl ::core::convert::From for v1::Bar { + fn from(__sv_bar: v1beta2::Bar) -> Self { + match __sv_bar { + v1beta2::Bar::Baz => v1::Bar::Baz, + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1beta2::Bar { + fn from(__sv_bar: v1::Bar) -> Self { + match __sv_bar { + v1::Bar::Baz => v1beta2::Bar::Baz, + } + } +} +#[automatically_derived] mod v1 { use super::*; /// Test @@ -192,6 +295,20 @@ mod v1 { pub baz: String, /// This is will keep changing over time. pub quux: String, + ///These are the docs from v1beta1 until v1. + /// + ///Multi-line docs are also supported. + #[doc(alias = "grault")] + pub corge: String, + /// The docs of this field changed in v1, while it was renamed in v1beta1. + pub fred: String, + } + /// Test + #[derive(Default)] + pub enum Bar { + /// The docs of this variant changed in v1. + #[default] + Baz, } } #[automatically_derived] @@ -207,6 +324,8 @@ where deprecated_bar: __sv_foo.deprecated_bar.into(), baz: __sv_foo.baz.into(), quux: __sv_foo.quux.into(), + corge: __sv_foo.corge.into(), + fred: __sv_foo.fred.into(), }; spec } @@ -224,11 +343,29 @@ where deprecated_bar: __sv_foo.deprecated_bar.into(), baz: __sv_foo.baz.into(), quux: __sv_foo.quux.into(), + corge: __sv_foo.corge.into(), + fred: __sv_foo.fred.into(), }; spec } } #[automatically_derived] +impl ::core::convert::From for v2::Bar { + fn from(__sv_bar: v1::Bar) -> Self { + match __sv_bar { + v1::Bar::Baz => v2::Bar::Baz, + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1::Bar { + fn from(__sv_bar: v2::Bar) -> Self { + match __sv_bar { + v2::Bar::Baz => v1::Bar::Baz, + } + } +} +#[automatically_derived] mod v2 { use super::*; /// Test @@ -243,5 +380,17 @@ mod v2 { pub baz: String, /// This is will keep changing over time. pub quux: String, + /// The docs of this field changed in v1beta1 and v2. + #[doc(alias = "grault")] + pub corge: String, + /// The docs of this field changed in v1, while it was renamed in v1beta1. + pub fred: String, + } + /// Test + #[derive(Default)] + pub enum Bar { + /// The docs of this variant changed in v1. + #[default] + Baz, } } diff --git a/crates/stackable-versioned/CHANGELOG.md b/crates/stackable-versioned/CHANGELOG.md index 3cf01678b..a2bc02e7a 100644 --- a/crates/stackable-versioned/CHANGELOG.md +++ b/crates/stackable-versioned/CHANGELOG.md @@ -8,9 +8,11 @@ All notable changes to this project will be documented in this file. - Add `#[versioned(hint(map))]` to provide a hint for map types during conversion ([#1285]). - Emit generics in plain `From` impl ([#1284]). +- Add `from_docs` argument to the `changed()` action to provide the previous doc comments of an item ([#XXXX]). [#1284]: https://github.com/stackabletech/operator-rs/pull/1284 [#1285]: https://github.com/stackabletech/operator-rs/pull/1285 +[#XXXX]: https://github.com/stackabletech/operator-rs/pull/XXXX ## [0.11.1] - 2026-07-06