Skip to content

Commit bfa35cb

Browse files
sbernauerTechassi
andauthored
feat(versioned): Add support for from_docs argument (#1293)
* feat(versioned): Add support for from_docs attribute * chore(versioned): Improve doc comment formatting * feat(versioned): Implement DocComments for (&)String * test(versioned): Update snapshot test to ensure attributes are preserved * chore(versioned): Add doc comments, rename previous_docs to changed_docs * docs(versioned): Tweak top-level doc comments * chore(versioned): Update changelog entry * chore: Temporarily allow RUSTSEC-2024-0449 advisory * chore: Revert formatting in deny.toml --------- Co-authored-by: Techassi <git@techassi.dev>
1 parent e901679 commit bfa35cb

21 files changed

Lines changed: 442 additions & 92 deletions

‎crates/stackable-versioned-macros/src/attrs/item/mod.rs‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use syn::{Attribute, Path, Type, spanned::Spanned};
88

99
use crate::{
1010
codegen::{VersionDefinition, item::ItemStatus},
11-
utils::ItemIdents,
11+
utils::{ItemIdents, doc_comments::DocComments as _},
1212
};
1313

1414
mod field;
@@ -223,9 +223,12 @@ impl CommonItemAttributes {
223223
let mut errors = Error::accumulator();
224224

225225
for change in &self.changes {
226-
if change.from_name.is_none() && change.from_type.is_none() {
226+
if change.from_name.is_none()
227+
&& change.from_type.is_none()
228+
&& change.from_docs.is_none()
229+
{
227230
errors.push(Error::custom(
228-
"both `from_name` and `from_type` are unset. Is this `changed()` action needed?"
231+
"`from_name`, `from_type` and `from_docs` are unset. Is this `changed()` action needed?"
229232
).with_span(&change.since.span()));
230233
}
231234

@@ -288,6 +291,26 @@ impl CommonItemAttributes {
288291
}
289292

290293
impl CommonItemAttributes {
294+
/// Returns a sparse map of the changed doc comments of the item, keyed by the version of the
295+
/// change.
296+
///
297+
/// Sparse in this context means that not every version is present as a key. Only versions
298+
/// containing a doc change are included. This is explicitly different from
299+
/// `VersionedField::changes` for which the code guarantees that there is an entry for all
300+
/// versions.
301+
//
302+
// NOTE (@Techassi): Potentially roll this into the changeset below so that there will be a
303+
// single sure of truth for field generation again.
304+
pub fn changed_docs(&self) -> BTreeMap<Version, Vec<String>> {
305+
self.changes
306+
.iter()
307+
.filter_map(|change| {
308+
let docs = change.from_docs.as_deref()?;
309+
Some((*change.since, docs.into_doc_comments()))
310+
})
311+
.collect()
312+
}
313+
291314
#[expect(clippy::too_many_lines)]
292315
pub fn into_changeset(
293316
self,
@@ -458,11 +481,13 @@ fn default_default_fn() -> SpannedValue<Path> {
458481
/// - `changed(since = "...", from_name = "...", from_type="...")`
459482
/// - `changed(since = "...", from_name = "...", from_type="...", upgrade_with = "...")`
460483
/// - `changed(since = "...", from_name = "...", from_type="...", downgrade_with = "...")`
484+
/// - `changed(since = "...", from_docs = "...")`
461485
#[derive(Clone, Debug, FromMeta)]
462486
pub struct ChangedAttributes {
463487
pub since: SpannedValue<Version>,
464488
pub from_name: Option<SpannedValue<String>>,
465489
pub from_type: Option<SpannedValue<Type>>,
490+
pub from_docs: Option<SpannedValue<String>>,
466491
pub upgrade_with: Option<SpannedValue<Path>>,
467492
pub downgrade_with: Option<SpannedValue<Path>>,
468493
}

‎crates/stackable-versioned-macros/src/codegen/item/field.rs‎

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::{
1111
codegen::{
1212
Direction, VersionDefinition,
1313
changes::{BTreeMapExt, ChangesetExt},
14-
item::ItemStatus,
14+
item::{ItemStatus, generate_attributes},
1515
module::ModuleGenerationContext,
1616
},
1717
utils::{ItemIdentExt, ItemIdents},
@@ -20,6 +20,16 @@ use crate::{
2020
#[derive(Debug)]
2121
pub struct VersionedField {
2222
pub original_attributes: Vec<Attribute>,
23+
24+
/// A sparse map of changed doc comments.
25+
///
26+
/// This map being sparse means that access to it must be able to deal with a version not being
27+
/// present. Using `unwrap()`, [`BTreeMapExt::get_expect`] or other similar functions is not
28+
/// recommended. Also see `CommonItemAttributes::changed_docs` for further details.
29+
//
30+
// NOTE (@Techassi): Potentially roll this into the changeset below so that there will be a
31+
// single sure of truth for field generation again.
32+
pub changed_docs: BTreeMap<Version, Vec<String>>,
2333
pub changes: Option<BTreeMap<Version, ItemStatus>>,
2434
pub idents: FieldIdents,
2535
pub hint: Option<Hint>,
@@ -45,6 +55,7 @@ impl VersionedField {
4555
})?;
4656
let idents = FieldIdents::from(ident);
4757

58+
let changed_docs = field_attributes.common.changed_docs();
4859
let changes = field_attributes
4960
.common
5061
.into_changeset(&idents, field.ty.clone());
@@ -53,6 +64,7 @@ impl VersionedField {
5364
Ok(Self {
5465
original_attributes: field_attributes.attrs,
5566
hint: field_attributes.hint,
67+
changed_docs,
5668
ty: field.ty,
5769
changes,
5870
idents,
@@ -82,7 +94,11 @@ impl VersionedField {
8294
/// }
8395
/// ```
8496
pub fn generate_for_container(&self, version: &VersionDefinition) -> Option<TokenStream> {
85-
let original_attributes = &self.original_attributes;
97+
let attributes = generate_attributes(
98+
&self.original_attributes,
99+
&self.changed_docs,
100+
&version.inner,
101+
);
86102

87103
#[allow(clippy::single_match_else)]
88104
match &self.changes {
@@ -106,13 +122,13 @@ impl VersionedField {
106122
)
107123
}) {
108124
ItemStatus::Addition { ident, ty, .. } => Some(quote! {
109-
#(#original_attributes)*
125+
#attributes
110126
pub #ident: #ty,
111127
}),
112128
ItemStatus::Change {
113129
to_ident, to_type, ..
114130
} => Some(quote! {
115-
#(#original_attributes)*
131+
#attributes
116132
pub #to_ident: #to_type,
117133
}),
118134
ItemStatus::Deprecation {
@@ -133,7 +149,7 @@ impl VersionedField {
133149
};
134150

135151
Some(quote! {
136-
#(#original_attributes)*
152+
#attributes
137153
#deprecated_attr
138154
pub #field_ident: #field_type,
139155
})
@@ -149,7 +165,7 @@ impl VersionedField {
149165
let deprecated_attr = previously_deprecated.then(|| quote! {#[deprecated]});
150166

151167
Some(quote! {
152-
#(#original_attributes)*
168+
#attributes
153169
#deprecated_attr
154170
pub #ident: #ty,
155171
})
@@ -163,7 +179,7 @@ impl VersionedField {
163179
let field_type = &self.ty;
164180

165181
Some(quote! {
166-
#(#original_attributes)*
182+
#attributes
167183
pub #field_ident: #field_type,
168184
})
169185
}

‎crates/stackable-versioned-macros/src/codegen/item/mod.rs‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,44 @@
1+
use std::{collections::BTreeMap, ops::Bound};
2+
13
use darling::util::IdentString;
2-
use syn::{Path, Type};
4+
use k8s_version::Version;
5+
use proc_macro2::TokenStream;
6+
use quote::quote;
7+
use syn::{Attribute, Meta, Path, Type};
8+
9+
use crate::codegen::changes::Neighbors as _;
310

411
mod field;
512
pub use field::*;
613

714
mod variant;
815
pub use variant::*;
916

17+
/// Generates the attributes of an item (field or variant) for the provided `version`.
18+
///
19+
/// If the docs of the item are changed in a later version (via `from_docs`), the original doc
20+
/// comments are replaced by the docs which are valid in `version`.
21+
pub fn generate_attributes(
22+
original_attributes: &[Attribute],
23+
changed_docs: &BTreeMap<Version, Vec<String>>,
24+
version: &Version,
25+
) -> TokenStream {
26+
// The docs valid in this version are the previous docs of the closest change after this
27+
// version. If there is no such change, the original docs are still valid.
28+
let Some((_, docs)) = changed_docs.up_bound(Bound::Excluded(version)) else {
29+
return quote! { #(#original_attributes)* };
30+
};
31+
32+
let attributes = original_attributes.iter().filter(|attribute| {
33+
!matches!(&attribute.meta, Meta::NameValue(name_value) if name_value.path.is_ident("doc"))
34+
});
35+
36+
quote! {
37+
#(#[doc = #docs])*
38+
#(#attributes)*
39+
}
40+
}
41+
1042
#[derive(Debug, PartialEq, Eq)]
1143
pub enum ItemStatus {
1244
Addition {

‎crates/stackable-versioned-macros/src/codegen/item/variant.rs‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ use crate::{
1313
codegen::{
1414
Direction, VersionDefinition,
1515
changes::{BTreeMapExt, ChangesetExt},
16-
item::ItemStatus,
16+
item::{ItemStatus, generate_attributes},
1717
},
1818
utils::ItemIdents,
1919
};
2020

2121
pub struct VersionedVariant {
2222
pub original_attributes: Vec<Attribute>,
23+
pub changed_docs: BTreeMap<Version, Vec<String>>,
2324
pub changes: Option<BTreeMap<Version, ItemStatus>>,
2425
pub idents: VariantIdents,
2526
pub fields: Fields,
@@ -39,10 +40,12 @@ impl VersionedVariant {
3940
attrs: Vec::new(),
4041
bang_token: Not([Span::call_site()]),
4142
});
43+
let changed_docs = variant_attributes.common.changed_docs();
4244
let changes = variant_attributes.common.into_changeset(&idents, ty);
4345

4446
Ok(Self {
4547
original_attributes: variant_attributes.attrs,
48+
changed_docs,
4649
fields: variant.fields,
4750
idents,
4851
changes,
@@ -63,7 +66,11 @@ impl VersionedVariant {
6366

6467
/// Generates tokens to be used in a container definition.
6568
pub fn generate_for_container(&self, version: &VersionDefinition) -> Option<TokenStream> {
66-
let original_attributes = &self.original_attributes;
69+
let attributes = generate_attributes(
70+
&self.original_attributes,
71+
&self.changed_docs,
72+
&version.inner,
73+
);
6774
let fields = &self.fields;
6875

6976
#[allow(clippy::single_match_else)]
@@ -80,11 +87,11 @@ impl VersionedVariant {
8087
)
8188
}) {
8289
ItemStatus::Addition { ident, .. } => Some(quote! {
83-
#(#original_attributes)*
90+
#attributes
8491
#ident #fields,
8592
}),
8693
ItemStatus::Change { to_ident, .. } => Some(quote! {
87-
#(#original_attributes)*
94+
#attributes
8895
#to_ident #fields,
8996
}),
9097
ItemStatus::Deprecation { ident, note, .. } => {
@@ -103,7 +110,7 @@ impl VersionedVariant {
103110
};
104111

105112
Some(quote! {
106-
#(#original_attributes)*
113+
#attributes
107114
#deprecated_attr
108115
#ident #fields,
109116
})
@@ -118,7 +125,7 @@ impl VersionedVariant {
118125
let deprecated_attr = previously_deprecated.then(|| quote! {#[deprecated]});
119126

120127
Some(quote! {
121-
#(#original_attributes)*
128+
#attributes
122129
#deprecated_attr
123130
#ident #fields,
124131
})
@@ -132,7 +139,7 @@ impl VersionedVariant {
132139
let ident = &self.idents.original;
133140

134141
Some(quote! {
135-
#(#original_attributes)*
142+
#attributes
136143
#ident #fields,
137144
})
138145
}

‎crates/stackable-versioned-macros/src/lib.rs‎

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -490,12 +490,14 @@ mod utils;
490490
/// ### Changed Action
491491
///
492492
/// This action indicates that an item is changed in a particular version. It
493-
/// combines renames and type changes into a single action. You can choose to
494-
/// change the name, change the type or do both. Available arguments are:
493+
/// combines renames, type changes and doc comment changes into a single action.
494+
/// You can choose to change the name, change the type, change the doc comment
495+
/// or combine multiple changes. Available arguments are:
495496
///
496497
/// - `since` to indicate since which version the item is changed.
497498
/// - `from_name` to indicate from which previous name the field is renamed.
498499
/// - `from_type` to indicate from which previous type the field is changed.
500+
/// - `from_docs` to provide the previous doc comments of the item.
499501
/// - `upgrade_with` to provide a custom upgrade function. This argument can
500502
/// only be used in combination with the `from_type` argument. The expected
501503
/// function signature is: `fn (OLD_TYPE) -> NEW_TYPE`. This function must
@@ -561,6 +563,58 @@ mod utils;
561563
/// ```
562564
/// </details>
563565
///
566+
/// #### Changed Docs
567+
///
568+
/// The doc comments of an item can be changed using the `from_docs` argument.
569+
/// The doc comments attached to the item are used after the version of the
570+
/// last relevant change. All earlier versions use the doc comments provided via
571+
/// `from_docs` instead. This is especially useful to adjust the description of
572+
/// a field in the generated CRD schema without changing it for older versions.
573+
///
574+
/// ```
575+
/// # use stackable_versioned_macros::versioned;
576+
/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))]
577+
/// mod versioned {
578+
/// pub struct Foo {
579+
/// /// The number of bars.
580+
/// #[versioned(changed(since = "v1beta1", from_docs = "The bar."))]
581+
/// bar: usize,
582+
/// baz: bool,
583+
/// }
584+
/// }
585+
/// ```
586+
///
587+
/// <details>
588+
/// <summary>Expand Generated Code</summary>
589+
///
590+
/// 1. In version `v1alpha1` the field uses the doc comments provided via
591+
/// `from_docs`.
592+
/// 2. In the next version, `v1beta1`, the field uses the doc comments attached
593+
/// to the field.
594+
///
595+
/// ```ignore
596+
/// pub mod v1alpha1 {
597+
/// use super::*;
598+
/// pub struct Foo {
599+
/// /// The bar. // 1
600+
/// pub bar: usize,
601+
/// pub baz: bool,
602+
/// }
603+
/// }
604+
///
605+
/// // Snip
606+
///
607+
/// pub mod v1beta1 {
608+
/// use super::*;
609+
/// pub struct Foo {
610+
/// /// The number of bars. // 2
611+
/// pub bar: usize,
612+
/// pub baz: bool,
613+
/// }
614+
/// }
615+
/// ```
616+
/// </details>
617+
///
564618
/// ### Deprecated Action
565619
///
566620
/// This action indicates that an item is deprecated in a particular version.

‎crates/stackable-versioned-macros/src/utils/doc_comments.rs‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,26 @@ impl DocComments for &str {
1212
.trim()
1313
.lines()
1414
// Trim the leading and trailing whitespace on each line that can be
15-
// introduced when the developer indents multi-line comments.
16-
.map(|line| line.trim().to_owned())
15+
// introduced when the developer indents multi-line comments. We then
16+
// re-add a leading space to be in line with all other, non-generated
17+
// dod comments.
18+
.map(|line| format!(" {line}", line = line.trim()))
1719
.collect()
1820
}
1921
}
2022

23+
impl DocComments for String {
24+
fn into_doc_comments(self) -> Vec<String> {
25+
self.as_str().into_doc_comments()
26+
}
27+
}
28+
29+
impl DocComments for &String {
30+
fn into_doc_comments(self) -> Vec<String> {
31+
self.as_str().into_doc_comments()
32+
}
33+
}
34+
2135
impl DocComments for Option<&str> {
2236
fn into_doc_comments(self) -> Vec<String> {
2337
self.map_or(vec![], DocComments::into_doc_comments)

0 commit comments

Comments
 (0)