diff --git a/crates/stackable-versioned-macros/src/attrs/common/container.rs b/crates/stackable-versioned-macros/src/attrs/common/container.rs index 94c1444b4..5888d45ca 100644 --- a/crates/stackable-versioned-macros/src/attrs/common/container.rs +++ b/crates/stackable-versioned-macros/src/attrs/common/container.rs @@ -96,11 +96,13 @@ impl ContainerAttributes { /// - `name` of the version, like `v1alpha1`. /// - `deprecated` flag to mark that version as deprecated. /// - `skip` option to skip generating various pieces of code. +/// - `doc` option to add version-specific documentation. #[derive(Clone, Debug, FromMeta)] pub(crate) struct VersionAttributes { pub(crate) deprecated: Flag, pub(crate) name: Version, pub(crate) skip: Option, + pub(crate) doc: Option, } /// This struct contains supported container options. diff --git a/crates/stackable-versioned-macros/src/attrs/common/item.rs b/crates/stackable-versioned-macros/src/attrs/common/item.rs index 430c6eb3b..744549a0e 100644 --- a/crates/stackable-versioned-macros/src/attrs/common/item.rs +++ b/crates/stackable-versioned-macros/src/attrs/common/item.rs @@ -1,7 +1,7 @@ use darling::{util::SpannedValue, Error, FromMeta}; use k8s_version::Version; use proc_macro2::Span; -use syn::{spanned::Spanned, Ident, Path}; +use syn::{spanned::Spanned, Attribute, Ident, Path}; use crate::{ attrs::common::ContainerAttributes, @@ -19,8 +19,8 @@ pub(crate) trait ValidateVersions where I: Spanned, { - /// Validates that each field action version is present in the declared - /// container versions. + /// Validates that each field or variant action version is present in the + /// declared container versions. fn validate_versions( &self, container_attrs: &ContainerAttributes, @@ -42,7 +42,7 @@ where let mut errors = Error::accumulator(); - if let Some(added) = &self.common_attrs().added { + if let Some(added) = &self.common_attributes().added { if !container_attrs .versions .iter() @@ -55,7 +55,7 @@ where } } - for rename in &*self.common_attrs().renames { + for rename in &*self.common_attributes().renames { if !container_attrs .versions .iter() @@ -68,7 +68,7 @@ where } } - if let Some(deprecated) = &self.common_attrs().deprecated { + if let Some(deprecated) = &self.common_attributes().deprecated { if !container_attrs .versions .iter() @@ -107,8 +107,8 @@ pub(crate) enum ItemType { /// is part of the container in every version until renamed or deprecated. /// - An item can be renamed many times. That's why renames are stored in a /// [`Vec`]. -/// - An item can only be deprecated once. A field not marked as 'deprecated' -/// will be included up until the latest version. +/// - An item can only be deprecated once. A field or variant not marked as +/// 'deprecated' will be included up until the latest version. #[derive(Debug, FromMeta)] pub(crate) struct ItemAttributes { /// This parses the `added` attribute on items (fields or variants). It can @@ -126,15 +126,20 @@ pub(crate) struct ItemAttributes { } impl ItemAttributes { - pub(crate) fn validate(&self, item_ident: &Ident, item_type: &ItemType) -> Result<(), Error> { + pub(crate) fn validate( + &self, + item_ident: &Ident, + item_type: &ItemType, + item_attrs: &Vec, + ) -> Result<(), Error> { // NOTE (@Techassi): This associated function is NOT called by darling's // and_then attribute, but instead by the wrapper, FieldAttributes and // VariantAttributes. let mut errors = Error::accumulator(); - // TODO (@Techassi): Make the field 'note' optional, because in the - // future, the macro will generate parts of the deprecation note + // TODO (@Techassi): Make the field or variant 'note' optional, because + // in the future, the macro will generate parts of the deprecation note // automatically. The user-provided note will then be appended to the // auto-generated one. @@ -150,10 +155,12 @@ impl ItemAttributes { // Semantic validation errors.handle(self.validate_action_combinations(item_ident, item_type)); errors.handle(self.validate_action_order(item_ident, item_type)); - errors.handle(self.validate_field_name(item_ident, item_type)); + errors.handle(self.validate_item_name(item_ident, item_type)); + errors.handle(self.validate_item_attributes(item_attrs)); - // TODO (@Techassi): Add hint if a field is added in the first version - // that it might be clever to remove the 'added' attribute. + // TODO (@Techassi): Add hint if a field or variant is added in the + // first version that it might be clever to remove the 'added' + // attribute. errors.finish()?; @@ -164,13 +171,13 @@ impl ItemAttributes { /// and validates that each item uses a valid combination of actions. /// Invalid combinations are: /// - /// - `added` and `deprecated` using the same version: A field cannot be - /// marked as added in a particular version and then marked as deprecated - /// immediately after. Fields must be included for at least one version - /// before being marked deprecated. + /// - `added` and `deprecated` using the same version: A field or variant + /// cannot be marked as added in a particular version and then marked as + /// deprecated immediately after. Fields and variants must be included for + /// at least one version before being marked deprecated. /// - `added` and `renamed` using the same version: The same reasoning from - /// above applies here as well. Fields must be included for at least one - /// version before being renamed. + /// above applies here as well. Fields and variants must be included for + /// at least one version before being renamed. /// - `renamed` and `deprecated` using the same version: Again, the same /// rules from above apply here as well. fn validate_action_combinations( @@ -195,7 +202,7 @@ impl ItemAttributes { if renamed.iter().any(|r| *r.since == *deprecated.since) => { Err(Error::custom( - "field cannot be marked as `deprecated` and `renamed` in the same version", + format!("{item_type} cannot be marked as `deprecated` and `renamed` in the same version"), ) .with_span(item_ident)) } @@ -252,10 +259,10 @@ impl ItemAttributes { /// /// The following naming rules apply: /// - /// - Fields marked as deprecated need to include the 'deprecated_' prefix - /// in their name. The prefix must not be included for fields which are - /// not deprecated. - fn validate_field_name(&self, item_ident: &Ident, item_type: &ItemType) -> Result<(), Error> { + /// - Fields or variants marked as deprecated need to include the + /// deprecation prefix in their name. The prefix must not be included for + /// fields or variants which are not deprecated. + fn validate_item_name(&self, item_ident: &Ident, item_type: &ItemType) -> Result<(), Error> { let prefix = match item_type { ItemType::Field => DEPRECATED_FIELD_PREFIX, ItemType::Variant => DEPRECATED_VARIANT_PREFIX, @@ -277,6 +284,25 @@ impl ItemAttributes { Ok(()) } + + /// This associated function is called by the top-level validation function + /// and validates that disallowed item attributes are not used. + /// + /// The following naming rules apply: + /// + /// - `deprecated` must not be set on items. Instead, use the `deprecated()` + /// action of the `#[versioned()]` macro. + fn validate_item_attributes(&self, item_attrs: &Vec) -> Result<(), Error> { + for attr in item_attrs { + for segment in &attr.path().segments { + if segment.ident == "deprecated" { + return Err(Error::custom("deprecation must be done using #[versioned(deprecated(since = \"VERSION\"))]") + .with_span(&attr.span())); + } + } + } + Ok(()) + } } /// For the added() action diff --git a/crates/stackable-versioned-macros/src/attrs/field.rs b/crates/stackable-versioned-macros/src/attrs/field.rs index 0f1e017b4..e2816d927 100644 --- a/crates/stackable-versioned-macros/src/attrs/field.rs +++ b/crates/stackable-versioned-macros/src/attrs/field.rs @@ -1,5 +1,5 @@ use darling::{Error, FromField}; -use syn::Ident; +use syn::{Attribute, Ident}; use crate::attrs::common::{ItemAttributes, ItemType}; @@ -19,7 +19,7 @@ use crate::attrs::common::{ItemAttributes, ItemType}; #[derive(Debug, FromField)] #[darling( attributes(versioned), - forward_attrs(allow, doc, cfg, serde), + forward_attrs, and_then = FieldAttributes::validate )] pub(crate) struct FieldAttributes { @@ -30,6 +30,12 @@ pub(crate) struct FieldAttributes { // shared item attributes because for struct fields, the type is // `Option`, while for enum variants, the type is `Ident`. pub(crate) ident: Option, + + // This must be named `attrs` for darling to populate it accordingly, and + // cannot live in common because Vec is not implemented for + // FromMeta. + /// The original attributes for the field. + pub(crate) attrs: Vec, } impl FieldAttributes { @@ -44,7 +50,7 @@ impl FieldAttributes { .ident .as_ref() .expect("internal error: field must have an ident"); - self.common.validate(ident, &ItemType::Field)?; + self.common.validate(ident, &ItemType::Field, &self.attrs)?; Ok(self) } diff --git a/crates/stackable-versioned-macros/src/attrs/variant.rs b/crates/stackable-versioned-macros/src/attrs/variant.rs index cb831e52b..ece8f189d 100644 --- a/crates/stackable-versioned-macros/src/attrs/variant.rs +++ b/crates/stackable-versioned-macros/src/attrs/variant.rs @@ -1,6 +1,6 @@ use convert_case::{Case, Casing}; use darling::{Error, FromVariant}; -use syn::Ident; +use syn::{Attribute, Ident}; use crate::attrs::common::{ItemAttributes, ItemType}; @@ -20,7 +20,7 @@ use crate::attrs::common::{ItemAttributes, ItemType}; #[derive(Debug, FromVariant)] #[darling( attributes(versioned), - forward_attrs(allow, doc, cfg, serde), + forward_attrs, and_then = VariantAttributes::validate )] pub(crate) struct VariantAttributes { @@ -31,6 +31,12 @@ pub(crate) struct VariantAttributes { // shared item attributes because for struct fields, the type is // `Option`, while for enum variants, the type is `Ident`. pub(crate) ident: Ident, + + // This must be named `attrs` for darling to populate it accordingly, and + // cannot live in common because Vec is not implemented for + // FromMeta. + /// The original attributes for the field. + pub(crate) attrs: Vec, } impl VariantAttributes { @@ -43,7 +49,10 @@ impl VariantAttributes { fn validate(self) -> Result { let mut errors = Error::accumulator(); - errors.handle(self.common.validate(&self.ident, &ItemType::Variant)); + errors.handle( + self.common + .validate(&self.ident, &ItemType::Variant, &self.attrs), + ); // Validate names of renames if !self diff --git a/crates/stackable-versioned-macros/src/codegen/common/container.rs b/crates/stackable-versioned-macros/src/codegen/common/container.rs index 0e211897a..22e8fcdae 100644 --- a/crates/stackable-versioned-macros/src/codegen/common/container.rs +++ b/crates/stackable-versioned-macros/src/codegen/common/container.rs @@ -1,7 +1,7 @@ use std::ops::Deref; use proc_macro2::TokenStream; -use syn::Ident; +use syn::{Attribute, Ident}; use crate::{attrs::common::ContainerAttributes, codegen::common::ContainerVersion}; @@ -21,7 +21,12 @@ where Self: Sized + Deref>, { /// Creates a new versioned container. - fn new(ident: Ident, data: D, attributes: ContainerAttributes) -> syn::Result; + fn new( + ident: Ident, + data: D, + attributes: ContainerAttributes, + original_attributes: Vec, + ) -> syn::Result; /// This generates the complete code for a single versioned container. /// @@ -32,12 +37,31 @@ where fn generate_tokens(&self) -> TokenStream; } +/// Stores individual versions of a single container. +/// +/// Each version tracks item actions, which describe if the item was added, +/// renamed or deprecated in that particular version. Items which are not +/// versioned are included in every version of the container. #[derive(Debug)] pub(crate) struct VersionedContainer { + /// List of declared versions for this container. Each version generates a + /// definition with appropriate items. pub(crate) versions: Vec, + + /// List of items defined in the original container. How, and if, an item + /// should generate code, is decided by the currently generated version. pub(crate) items: Vec, + + /// The ident, or name, of the versioned container. pub(crate) ident: Ident, + /// The name of the container used in `From` implementations. pub(crate) from_ident: Ident, + + /// Whether the [`From`] implementation generation should be skipped for all + /// versions of this container. pub(crate) skip_from: bool, + + /// The original attributes that were added to the container. + pub(crate) original_attributes: Vec, } diff --git a/crates/stackable-versioned-macros/src/codegen/common/item.rs b/crates/stackable-versioned-macros/src/codegen/common/item.rs index 347e22c7c..6baf1ff34 100644 --- a/crates/stackable-versioned-macros/src/codegen/common/item.rs +++ b/crates/stackable-versioned-macros/src/codegen/common/item.rs @@ -1,7 +1,7 @@ use std::{collections::BTreeMap, marker::PhantomData, ops::Deref}; use quote::format_ident; -use syn::{spanned::Spanned, Ident, Path}; +use syn::{spanned::Spanned, Attribute, Ident, Path}; use crate::{ attrs::common::{ContainerAttributes, ItemAttributes, ValidateVersions}, @@ -53,11 +53,17 @@ pub(crate) trait Named { fn ident(&self) -> &Ident; } -/// This trait enables access to the common attributes across field and variant -/// attributes. +/// This trait enables access to the common and original attributes across field +/// and variant attributes. pub(crate) trait Attributes { - fn common_attrs_owned(self) -> ItemAttributes; - fn common_attrs(&self) -> &ItemAttributes; + /// The common attributes defined by the versioned macro. + fn common_attributes_owned(self) -> ItemAttributes; + + /// The common attributes defined by the versioned macro. + fn common_attributes(&self) -> &ItemAttributes; + + /// The attributes applied to the item outside of the versioned macro. + fn original_attributes(&self) -> &Vec; } /// This struct combines common code for versioned fields and variants. @@ -85,6 +91,7 @@ where { pub(crate) chain: Option, pub(crate) inner: I, + pub(crate) original_attributes: Vec, _marker: PhantomData, } @@ -103,7 +110,11 @@ where let attrs = A::try_from(&item)?; attrs.validate_versions(container_attrs, &item)?; - let item_attrs = attrs.common_attrs_owned(); + // These are the attributes added to the item outside of the macro. + let original_attributes = attrs.original_attributes().clone(); + + // These are the versioned macro attrs that are common to all items. + let common_attributes = attrs.common_attributes_owned(); // Constructing the action chain requires going through the actions // starting at the end, because the container definition always @@ -117,7 +128,7 @@ where // latest rename or addition, which is handled below. The ident of the // deprecated item is guaranteed to include the 'deprecated_' or // 'DEPRECATED_' prefix. The ident can thus be used as is. - if let Some(deprecated) = item_attrs.deprecated { + if let Some(deprecated) = common_attributes.deprecated { let deprecated_ident = item.ident(); // When the item is deprecated, any rename which occurred beforehand @@ -135,7 +146,7 @@ where }, ); - for rename in item_attrs.renames.iter().rev() { + for rename in common_attributes.renames.iter().rev() { let from = format_ident!("{from}", from = *rename.from); actions.insert( *rename.since, @@ -149,7 +160,7 @@ where // After the last iteration above (if any) we use the ident for the // added action if there is any. - if let Some(added) = item_attrs.added { + if let Some(added) = common_attributes.added { actions.insert( *added.since, ItemStatus::Added { @@ -163,12 +174,13 @@ where _marker: PhantomData, chain: Some(actions), inner: item, + original_attributes, }) - } else if !item_attrs.renames.is_empty() { + } else if !common_attributes.renames.is_empty() { let mut actions = BTreeMap::new(); let mut ident = item.ident().clone(); - for rename in item_attrs.renames.iter().rev() { + for rename in common_attributes.renames.iter().rev() { let from = format_ident!("{from}", from = *rename.from); actions.insert( *rename.since, @@ -182,7 +194,7 @@ where // After the last iteration above (if any) we use the ident for the // added action if there is any. - if let Some(added) = item_attrs.added { + if let Some(added) = common_attributes.added { actions.insert( *added.since, ItemStatus::Added { @@ -196,9 +208,10 @@ where _marker: PhantomData, chain: Some(actions), inner: item, + original_attributes, }) } else { - if let Some(added) = item_attrs.added { + if let Some(added) = common_attributes.added { let mut actions = BTreeMap::new(); actions.insert( @@ -213,6 +226,7 @@ where _marker: PhantomData, chain: Some(actions), inner: item, + original_attributes, }); } @@ -220,6 +234,7 @@ where _marker: PhantomData, chain: None, inner: item, + original_attributes, }) } } diff --git a/crates/stackable-versioned-macros/src/codegen/common/mod.rs b/crates/stackable-versioned-macros/src/codegen/common/mod.rs index 2f416a88a..ad35ae1f0 100644 --- a/crates/stackable-versioned-macros/src/codegen/common/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/common/mod.rs @@ -32,6 +32,26 @@ pub(crate) struct ContainerVersion { /// The ident of the container. pub(crate) ident: Ident, + + /// Store additional doc-comment lines for this version. + pub(crate) version_specific_docs: Vec, +} + +/// Converts lines of doc-comments into a trimmed list. +fn process_docs(input: &Option) -> Vec { + if let Some(input) = input { + input + // Trim the leading and trailing whitespace, deleting suprefluous + // empty lines. + .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()) + .collect() + } else { + Vec::new() + } } impl From<&ContainerAttributes> for Vec { @@ -44,6 +64,7 @@ impl From<&ContainerAttributes> for Vec { ident: Ident::new(&v.name.to_string(), Span::call_site()), deprecated: v.deprecated.is_present(), inner: v.name, + version_specific_docs: process_docs(&v.doc), }) .collect() } diff --git a/crates/stackable-versioned-macros/src/codegen/mod.rs b/crates/stackable-versioned-macros/src/codegen/mod.rs index da964e988..ba2eefa9f 100644 --- a/crates/stackable-versioned-macros/src/codegen/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/mod.rs @@ -26,9 +26,11 @@ pub(crate) mod vstruct; pub(crate) fn expand(attributes: ContainerAttributes, input: DeriveInput) -> Result { let expanded = match input.data { Data::Struct(data) => { - VersionedStruct::new(input.ident, data, attributes)?.generate_tokens() + VersionedStruct::new(input.ident, data, attributes, input.attrs)?.generate_tokens() + } + Data::Enum(data) => { + VersionedEnum::new(input.ident, data, attributes, input.attrs)?.generate_tokens() } - Data::Enum(data) => VersionedEnum::new(input.ident, data, attributes)?.generate_tokens(), _ => { return Err(Error::new( input.span(), diff --git a/crates/stackable-versioned-macros/src/codegen/venum/mod.rs b/crates/stackable-versioned-macros/src/codegen/venum/mod.rs index 46db0af71..bf4f52642 100644 --- a/crates/stackable-versioned-macros/src/codegen/venum/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/venum/mod.rs @@ -3,7 +3,7 @@ use std::ops::Deref; use itertools::Itertools; use proc_macro2::TokenStream; use quote::quote; -use syn::{DataEnum, Error, Ident}; +use syn::{Attribute, DataEnum, Error, Ident}; use crate::{ attrs::common::ContainerAttributes, @@ -33,7 +33,12 @@ impl Deref for VersionedEnum { } impl Container for VersionedEnum { - fn new(ident: Ident, data: DataEnum, attributes: ContainerAttributes) -> syn::Result { + fn new( + ident: Ident, + data: DataEnum, + attributes: ContainerAttributes, + original_attributes: Vec, + ) -> syn::Result { // Convert the raw version attributes into a container version. let versions: Vec<_> = (&attributes).into(); @@ -77,6 +82,7 @@ impl Container for VersionedEnum { versions, items, ident, + original_attributes, })) } @@ -100,6 +106,7 @@ impl VersionedEnum { ) -> TokenStream { let mut token_stream = TokenStream::new(); let enum_name = &self.ident; + let original_attributes = &self.original_attributes; // Generate variants of the enum for `version`. let variants = self.generate_enum_variants(version); @@ -115,11 +122,27 @@ impl VersionedEnum { .deprecated .then_some(quote! {#[deprecated = #deprecated_note]}); + let mut version_specific_docs = TokenStream::new(); + for (i, doc) in version.version_specific_docs.iter().enumerate() { + if i == 0 { + // Prepend an empty line to clearly separate the version + // specific docs. + version_specific_docs.extend(quote! { + #[doc = ""] + }) + } + version_specific_docs.extend(quote! { + #[doc = #doc] + }) + } + // Generate tokens for the module and the contained enum token_stream.extend(quote! { #[automatically_derived] #deprecated_attr pub mod #version_ident { + #(#original_attributes)* + #version_specific_docs pub enum #enum_name { #variants } diff --git a/crates/stackable-versioned-macros/src/codegen/venum/variant.rs b/crates/stackable-versioned-macros/src/codegen/venum/variant.rs index 1a559ca99..e2b7eb6cf 100644 --- a/crates/stackable-versioned-macros/src/codegen/venum/variant.rs +++ b/crates/stackable-versioned-macros/src/codegen/venum/variant.rs @@ -51,13 +51,17 @@ impl TryFrom<&Variant> for VariantAttributes { } impl Attributes for VariantAttributes { - fn common_attrs_owned(self) -> ItemAttributes { + fn common_attributes_owned(self) -> ItemAttributes { self.common } - fn common_attrs(&self) -> &ItemAttributes { + fn common_attributes(&self) -> &ItemAttributes { &self.common } + + fn original_attributes(&self) -> &Vec { + &self.attrs + } } impl Named for Variant { @@ -88,6 +92,8 @@ impl VersionedVariant { &self, container_version: &ContainerVersion, ) -> Option { + let original_attributes = &self.original_attributes; + match &self.chain { // NOTE (@Techassi): https://rust-lang.github.io/rust-clippy/master/index.html#/expect_fun_call Some(chain) => match chain.get(&container_version.inner).unwrap_or_else(|| { @@ -97,16 +103,20 @@ impl VersionedVariant { ) }) { ItemStatus::Added { ident, .. } => Some(quote! { + #(#original_attributes)* #ident, }), ItemStatus::Renamed { to, .. } => Some(quote! { + #(#original_attributes)* #to, }), ItemStatus::Deprecated { ident, .. } => Some(quote! { + #(#original_attributes)* #[deprecated] #ident, }), ItemStatus::NoChange(ident) => Some(quote! { + #(#original_attributes)* #ident, }), ItemStatus::NotPresent => None, @@ -118,6 +128,7 @@ impl VersionedVariant { let variant_ident = &self.inner.ident; Some(quote! { + #(#original_attributes)* #variant_ident, }) } diff --git a/crates/stackable-versioned-macros/src/codegen/vstruct/field.rs b/crates/stackable-versioned-macros/src/codegen/vstruct/field.rs index 17bb3eafc..579cb13e4 100644 --- a/crates/stackable-versioned-macros/src/codegen/vstruct/field.rs +++ b/crates/stackable-versioned-macros/src/codegen/vstruct/field.rs @@ -50,13 +50,17 @@ impl TryFrom<&Field> for FieldAttributes { } impl Attributes for FieldAttributes { - fn common_attrs_owned(self) -> ItemAttributes { + fn common_attributes_owned(self) -> ItemAttributes { self.common } - fn common_attrs(&self) -> &ItemAttributes { + fn common_attributes(&self) -> &ItemAttributes { &self.common } + + fn original_attributes(&self) -> &Vec { + &self.attrs + } } impl Named for Field { @@ -90,6 +94,8 @@ impl VersionedField { &self, container_version: &ContainerVersion, ) -> Option { + let original_attributes = &self.original_attributes; + match &self.chain { Some(chain) => { // Check if the provided container version is present in the map @@ -110,9 +116,11 @@ impl VersionedField { ) }) { ItemStatus::Added { ident, .. } => Some(quote! { + #(#original_attributes)* pub #ident: #field_type, }), ItemStatus::Renamed { to, .. } => Some(quote! { + #(#original_attributes)* pub #to: #field_type, }), ItemStatus::Deprecated { @@ -120,11 +128,13 @@ impl VersionedField { note, .. } => Some(quote! { + #(#original_attributes)* #[deprecated = #note] pub #field_ident: #field_type, }), ItemStatus::NotPresent => None, ItemStatus::NoChange(field_ident) => Some(quote! { + #(#original_attributes)* pub #field_ident: #field_type, }), } @@ -136,6 +146,7 @@ impl VersionedField { let field_type = &self.inner.ty; Some(quote! { + #(#original_attributes)* pub #field_ident: #field_type, }) } diff --git a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs b/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs index b0fefaa5d..cebe66765 100644 --- a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs @@ -3,7 +3,7 @@ use std::ops::Deref; use itertools::Itertools; use proc_macro2::TokenStream; use quote::quote; -use syn::{DataStruct, Error, Ident}; +use syn::{Attribute, DataStruct, Error, Ident}; use crate::{ attrs::common::ContainerAttributes, @@ -33,7 +33,12 @@ impl Deref for VersionedStruct { } impl Container for VersionedStruct { - fn new(ident: Ident, data: DataStruct, attributes: ContainerAttributes) -> syn::Result { + fn new( + ident: Ident, + data: DataStruct, + attributes: ContainerAttributes, + original_attributes: Vec, + ) -> syn::Result { // Convert the raw version attributes into a container version. let versions: Vec<_> = (&attributes).into(); @@ -77,6 +82,7 @@ impl Container for VersionedStruct { versions, items, ident, + original_attributes, })) } @@ -100,6 +106,7 @@ impl VersionedStruct { ) -> TokenStream { let mut token_stream = TokenStream::new(); let struct_name = &self.ident; + let original_attributes = &self.original_attributes; // Generate fields of the struct for `version`. let fields = self.generate_struct_fields(version); @@ -115,11 +122,27 @@ impl VersionedStruct { .deprecated .then_some(quote! {#[deprecated = #deprecated_note]}); + let mut version_specific_docs = TokenStream::new(); + for (i, doc) in version.version_specific_docs.iter().enumerate() { + if i == 0 { + // Prepend an empty line to clearly separate the version + // specific docs. + version_specific_docs.extend(quote! { + #[doc = ""] + }) + } + version_specific_docs.extend(quote! { + #[doc = #doc] + }) + } + // Generate tokens for the module and the contained struct token_stream.extend(quote! { #[automatically_derived] #deprecated_attr pub mod #version_ident { + #(#original_attributes)* + #version_specific_docs pub struct #struct_name { #fields } diff --git a/crates/stackable-versioned-macros/tests/attributes.rs b/crates/stackable-versioned-macros/tests/attributes.rs new file mode 100644 index 000000000..929bf73d3 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/attributes.rs @@ -0,0 +1,106 @@ +use stackable_versioned_macros::versioned; + +#[ignore] +#[test] +fn pass_struct_attributes() { + /// General struct docs that cover all versions. + #[versioned( + version(name = "v1alpha1"), + version( + name = "v1beta1", + doc = r#" + 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. + "# + ), + version(name = "v1beta2"), + version(name = "v1"), + version(name = "v2"), + options(skip(from)) + )] + #[derive(Default)] + struct Foo { + /// This field is available in every version (so far). + foo: String, + + /// Keep the main field docs the same, even after the field is deprecated. + #[versioned(deprecated(since = "v1beta1", note = "gone"))] + deprecated_bar: String, + + /// This is for baz + #[versioned(added(since = "v1beta1"))] + baz: String, + + /// This is will keep changing over time. + #[versioned(renamed(since = "v1beta1", from = "qoox"))] + #[versioned(renamed(since = "v1", from = "qaax"))] + quux: String, + } + + let _ = v1alpha1::Foo { + foo: String::from("foo"), + bar: String::from("Hello"), + qoox: String::from("world"), + }; + + #[allow(deprecated)] + let _ = v1beta1::Foo { + foo: String::from("foo"), + deprecated_bar: String::from("Hello"), + baz: String::from("Hello"), + qaax: String::from("World"), + }; + + #[allow(deprecated)] + let _ = v1::Foo { + foo: String::from("foo"), + deprecated_bar: String::from("Hello"), + baz: String::from("Hello"), + quux: String::from("World"), + }; +} + +#[ignore] +#[allow(dead_code)] +#[test] +fn pass_enum_attributes() { + /// General enum docs that cover all versions. + #[versioned( + version(name = "v1alpha1"), + version( + name = "v1beta1", + doc = r#" + 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. + "# + ), + version(name = "v1beta2"), + version(name = "v1"), + version(name = "v2"), + options(skip(from)) + )] + #[derive(Default)] + enum Foo { + /// This variant is available in every version (so far). + #[default] + Foo, + + /// Keep the main field docs the same, even after the field is + /// deprecated. + #[versioned(deprecated(since = "v1beta1", note = "gone"))] + DeprecatedBar, + + /// This is for baz + #[versioned(added(since = "v1beta1"))] + // Just to check stackable-versioned deprecation warning appears. + // #[deprecated] + Baz, + + /// This is will keep changing over time. + #[versioned(renamed(since = "v1beta1", from = "Qoox"))] + #[versioned(renamed(since = "v1", from = "Qaax"))] + Quux, + } +} diff --git a/crates/stackable-versioned/CHANGELOG.md b/crates/stackable-versioned/CHANGELOG.md index 764e01445..e7bdd819c 100644 --- a/crates/stackable-versioned/CHANGELOG.md +++ b/crates/stackable-versioned/CHANGELOG.md @@ -4,12 +4,18 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Pass through container and item attributes (including doc-comments). Add + attribute for version specific docs. ([#847]) + ### Fixed - Report variant rename validation error at the correct span and trim underscores from variants not using PascalCase (#[842]). [#842]: https://github.com/stackabletech/operator-rs/pull/842 +[#847]: https://github.com/stackabletech/operator-rs/pull/847 ## [0.1.1] - 2024-07-10