Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 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,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<Version, Vec<String>> {
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,
Expand Down Expand Up @@ -458,11 +473,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
21 changes: 14 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,7 @@ use crate::{
#[derive(Debug)]
pub struct VersionedField {
pub original_attributes: Vec<Attribute>,
pub previous_docs: BTreeMap<Version, Vec<String>>,
pub changes: Option<BTreeMap<Version, ItemStatus>>,
pub idents: FieldIdents,
pub hint: Option<Hint>,
Expand All @@ -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());
Expand All @@ -53,6 +55,7 @@ impl VersionedField {
Ok(Self {
original_attributes: field_attributes.attrs,
hint: field_attributes.hint,
previous_docs,
ty: field.ty,
changes,
idents,
Expand Down Expand Up @@ -82,7 +85,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.previous_docs,
&version.inner,
);

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

Some(quote! {
#(#original_attributes)*
#attributes
#deprecated_attr
pub #field_ident: #field_type,
})
Expand All @@ -149,7 +156,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 +170,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],
previous_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)) = 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 {
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 previous_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 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,
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.previous_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
54 changes: 54 additions & 0 deletions crates/stackable-versioned-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -561,6 +562,59 @@ 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 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,
/// }
/// }
/// ```
///
/// <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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions crates/stackable-versioned-macros/tests/inputs/pass/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
// ---
Expand Down
Loading
Loading