Skip to content
31 changes: 28 additions & 3 deletions crates/stackable-versioned-macros/src/attrs/item/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()));
}

Expand Down Expand Up @@ -288,6 +291,26 @@ impl CommonItemAttributes {
}

impl CommonItemAttributes {
/// Returns a sparse map of the changed doc comments of the item, keyed by the version of the
/// change.
///
/// Sparse in this context means that not every version is present as a key. Only versions
/// containing a doc change are included. This is explicitly different from
/// `VersionedField::changes` for which the code guarantees that there is an entry for all
/// versions.
//
// NOTE (@Techassi): Potentially roll this into the changeset below so that there will be a
// single sure of truth for field generation again.
pub fn changed_docs(&self) -> BTreeMap<Version, Vec<String>> {
self.changes
.iter()
.filter_map(|change| {
let docs = change.from_docs.as_deref()?;
Some((*change.since, docs.into_doc_comments()))
})
.collect()
}

#[expect(clippy::too_many_lines)]
pub fn into_changeset(
self,
Expand Down Expand Up @@ -458,11 +481,13 @@ fn default_default_fn() -> SpannedValue<Path> {
/// - `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<Version>,
pub from_name: Option<SpannedValue<String>>,
pub from_type: Option<SpannedValue<Type>>,
pub from_docs: Option<SpannedValue<String>>,
pub upgrade_with: Option<SpannedValue<Path>>,
pub downgrade_with: Option<SpannedValue<Path>>,
}
Expand Down
30 changes: 23 additions & 7 deletions crates/stackable-versioned-macros/src/codegen/item/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
codegen::{
Direction, VersionDefinition,
changes::{BTreeMapExt, ChangesetExt},
item::ItemStatus,
item::{ItemStatus, generate_attributes},
module::ModuleGenerationContext,
},
utils::{ItemIdentExt, ItemIdents},
Expand All @@ -20,6 +20,16 @@ use crate::{
#[derive(Debug)]
pub struct VersionedField {
pub original_attributes: Vec<Attribute>,

/// A sparse map of changed doc comments.
///
/// This map being sparse means that access to it must be able to deal with a version not being
/// present. Using `unwrap()`, [`BTreeMapExt::get_expect`] or other similar functions is not
/// recommended. Also see `CommonItemAttributes::changed_docs` for further details.
//
// NOTE (@Techassi): Potentially roll this into the changeset below so that there will be a
// single sure of truth for field generation again.
pub changed_docs: BTreeMap<Version, Vec<String>>,
pub changes: Option<BTreeMap<Version, ItemStatus>>,
pub idents: FieldIdents,
pub hint: Option<Hint>,
Expand All @@ -45,6 +55,7 @@ impl VersionedField {
})?;
let idents = FieldIdents::from(ident);

let changed_docs = field_attributes.common.changed_docs();
let changes = field_attributes
.common
.into_changeset(&idents, field.ty.clone());
Expand All @@ -53,6 +64,7 @@ impl VersionedField {
Ok(Self {
original_attributes: field_attributes.attrs,
hint: field_attributes.hint,
changed_docs,
ty: field.ty,
changes,
idents,
Expand Down Expand Up @@ -82,7 +94,11 @@ impl VersionedField {
/// }
/// ```
pub fn generate_for_container(&self, version: &VersionDefinition) -> Option<TokenStream> {
let original_attributes = &self.original_attributes;
let attributes = generate_attributes(
&self.original_attributes,
&self.changed_docs,
&version.inner,
);

#[allow(clippy::single_match_else)]
match &self.changes {
Expand All @@ -106,13 +122,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 {
Expand All @@ -133,7 +149,7 @@ impl VersionedField {
};

Some(quote! {
#(#original_attributes)*
#attributes
#deprecated_attr
pub #field_ident: #field_type,
})
Expand All @@ -149,7 +165,7 @@ impl VersionedField {
let deprecated_attr = previously_deprecated.then(|| quote! {#[deprecated]});

Some(quote! {
#(#original_attributes)*
#attributes
#deprecated_attr
pub #ident: #ty,
})
Expand All @@ -163,7 +179,7 @@ impl VersionedField {
let field_type = &self.ty;

Some(quote! {
#(#original_attributes)*
#attributes
pub #field_ident: #field_type,
})
}
Expand Down
34 changes: 33 additions & 1 deletion crates/stackable-versioned-macros/src/codegen/item/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,44 @@
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::*;

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],
changed_docs: &BTreeMap<Version, Vec<String>>,
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)) = changed_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 {
Expand Down
21 changes: 14 additions & 7 deletions crates/stackable-versioned-macros/src/codegen/item/variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Attribute>,
pub changed_docs: BTreeMap<Version, Vec<String>>,
pub changes: Option<BTreeMap<Version, ItemStatus>>,
pub idents: VariantIdents,
pub fields: Fields,
Expand All @@ -39,10 +40,12 @@ impl VersionedVariant {
attrs: Vec::new(),
bang_token: Not([Span::call_site()]),
});
let changed_docs = variant_attributes.common.changed_docs();
let changes = variant_attributes.common.into_changeset(&idents, ty);

Ok(Self {
original_attributes: variant_attributes.attrs,
changed_docs,
fields: variant.fields,
idents,
changes,
Expand All @@ -63,7 +66,11 @@ impl VersionedVariant {

/// Generates tokens to be used in a container definition.
pub fn generate_for_container(&self, version: &VersionDefinition) -> Option<TokenStream> {
let original_attributes = &self.original_attributes;
let attributes = generate_attributes(
&self.original_attributes,
&self.changed_docs,
&version.inner,
);
let fields = &self.fields;

#[allow(clippy::single_match_else)]
Expand All @@ -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, .. } => {
Expand All @@ -103,7 +110,7 @@ impl VersionedVariant {
};

Some(quote! {
#(#original_attributes)*
#attributes
#deprecated_attr
#ident #fields,
})
Expand All @@ -118,7 +125,7 @@ impl VersionedVariant {
let deprecated_attr = previously_deprecated.then(|| quote! {#[deprecated]});

Some(quote! {
#(#original_attributes)*
#attributes
#deprecated_attr
#ident #fields,
})
Expand All @@ -132,7 +139,7 @@ impl VersionedVariant {
let ident = &self.idents.original;

Some(quote! {
#(#original_attributes)*
#attributes
#ident #fields,
})
}
Expand Down
58 changes: 56 additions & 2 deletions crates/stackable-versioned-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,12 +490,14 @@ mod utils;
/// ### Changed Action
///
/// This action indicates that an item is changed in a particular version. It
/// combines renames and type changes into a single action. You can choose to
/// change the name, change the type or do both. Available arguments are:
/// combines renames, type changes and doc comment changes into a single action.
/// You can choose to change the name, change the type, change the doc comment
/// or combine multiple changes. Available arguments are:
///
/// - `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
Expand Down Expand Up @@ -561,6 +563,58 @@ mod utils;
/// ```
/// </details>
///
/// #### 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 after the version of the
/// last relevant 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.
///
/// ```
/// # 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,
/// }
/// }
/// ```
///
/// <details>
/// <summary>Expand Generated Code</summary>
///
/// 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,
/// }
/// }
/// ```
/// </details>
///
/// ### Deprecated Action
///
/// This action indicates that an item is deprecated in a particular version.
Expand Down
18 changes: 16 additions & 2 deletions crates/stackable-versioned-macros/src/utils/doc_comments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,26 @@ impl DocComments for &str {
.trim()
.lines()
// Trim the leading and trailing whitespace on each line that can be
// introduced when the developer indents multi-line comments.
.map(|line| line.trim().to_owned())
// introduced when the developer indents multi-line comments. We then
// re-add a leading space to be in line with all other, non-generated
// dod comments.
.map(|line| format!(" {line}", line = line.trim()))
.collect()
}
}

impl DocComments for String {
fn into_doc_comments(self) -> Vec<String> {
self.as_str().into_doc_comments()
}
}

impl DocComments for &String {
fn into_doc_comments(self) -> Vec<String> {
self.as_str().into_doc_comments()
}
}

impl DocComments for Option<&str> {
fn into_doc_comments(self) -> Vec<String> {
self.map_or(vec![], DocComments::into_doc_comments)
Expand Down
Loading
Loading