Skip to content

feat(stackable-versioned-macros): Handle attribute forwarding #847

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 22, 2024
Merged
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
Original file line number Diff line number Diff line change
@@ -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<SkipOptions>,
pub(crate) doc: Option<String>,
}

/// This struct contains supported container options.
76 changes: 51 additions & 25 deletions crates/stackable-versioned-macros/src/attrs/common/item.rs
Original file line number Diff line number Diff line change
@@ -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<I>
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<Attribute>,
) -> 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<Attribute>) -> 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
12 changes: 9 additions & 3 deletions crates/stackable-versioned-macros/src/attrs/field.rs
Original file line number Diff line number Diff line change
@@ -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<Ident>`, while for enum variants, the type is `Ident`.
pub(crate) ident: Option<Ident>,

// This must be named `attrs` for darling to populate it accordingly, and
// cannot live in common because Vec<Attribute> is not implemented for
// FromMeta.
/// The original attributes for the field.
pub(crate) attrs: Vec<Attribute>,
}

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)
}
15 changes: 12 additions & 3 deletions crates/stackable-versioned-macros/src/attrs/variant.rs
Original file line number Diff line number Diff line change
@@ -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<Ident>`, 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<Attribute> is not implemented for
// FromMeta.
/// The original attributes for the field.
pub(crate) attrs: Vec<Attribute>,
}

impl VariantAttributes {
@@ -43,7 +49,10 @@ impl VariantAttributes {
fn validate(self) -> Result<Self, Error> {
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
28 changes: 26 additions & 2 deletions crates/stackable-versioned-macros/src/codegen/common/container.rs
Original file line number Diff line number Diff line change
@@ -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<Target = VersionedContainer<I>>,
{
/// Creates a new versioned container.
fn new(ident: Ident, data: D, attributes: ContainerAttributes) -> syn::Result<Self>;
fn new(
ident: Ident,
data: D,
attributes: ContainerAttributes,
original_attributes: Vec<Attribute>,
) -> syn::Result<Self>;

/// 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<I> {
/// List of declared versions for this container. Each version generates a
/// definition with appropriate items.
pub(crate) versions: Vec<ContainerVersion>,

/// 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<I>,

/// 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<Attribute>,
}
41 changes: 28 additions & 13 deletions crates/stackable-versioned-macros/src/codegen/common/item.rs
Original file line number Diff line number Diff line change
@@ -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<Attribute>;
}

/// This struct combines common code for versioned fields and variants.
@@ -85,6 +91,7 @@ where
{
pub(crate) chain: Option<VersionChain>,
pub(crate) inner: I,
pub(crate) original_attributes: Vec<Attribute>,
_marker: PhantomData<A>,
}

@@ -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,13 +226,15 @@ where
_marker: PhantomData,
chain: Some(actions),
inner: item,
original_attributes,
});
}

Ok(Self {
_marker: PhantomData,
chain: None,
inner: item,
original_attributes,
})
}
}
21 changes: 21 additions & 0 deletions crates/stackable-versioned-macros/src/codegen/common/mod.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

/// Converts lines of doc-comments into a trimmed list.
fn process_docs(input: &Option<String>) -> Vec<String> {
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<ContainerVersion> {
@@ -44,6 +64,7 @@ impl From<&ContainerAttributes> for Vec<ContainerVersion> {
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()
}
6 changes: 4 additions & 2 deletions crates/stackable-versioned-macros/src/codegen/mod.rs
Original file line number Diff line number Diff line change
@@ -26,9 +26,11 @@ pub(crate) mod vstruct;
pub(crate) fn expand(attributes: ContainerAttributes, input: DeriveInput) -> Result<TokenStream> {
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(),
27 changes: 25 additions & 2 deletions crates/stackable-versioned-macros/src/codegen/venum/mod.rs
Original file line number Diff line number Diff line change
@@ -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<DataEnum, VersionedVariant> for VersionedEnum {
fn new(ident: Ident, data: DataEnum, attributes: ContainerAttributes) -> syn::Result<Self> {
fn new(
ident: Ident,
data: DataEnum,
attributes: ContainerAttributes,
original_attributes: Vec<Attribute>,
) -> syn::Result<Self> {
// Convert the raw version attributes into a container version.
let versions: Vec<_> = (&attributes).into();

@@ -77,6 +82,7 @@ impl Container<DataEnum, VersionedVariant> 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
}
15 changes: 13 additions & 2 deletions crates/stackable-versioned-macros/src/codegen/venum/variant.rs
Original file line number Diff line number Diff line change
@@ -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<syn::Attribute> {
&self.attrs
}
}

impl Named for Variant {
@@ -88,6 +92,8 @@ impl VersionedVariant {
&self,
container_version: &ContainerVersion,
) -> Option<TokenStream> {
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,
})
}
15 changes: 13 additions & 2 deletions crates/stackable-versioned-macros/src/codegen/vstruct/field.rs
Original file line number Diff line number Diff line change
@@ -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<syn::Attribute> {
&self.attrs
}
}

impl Named for Field {
@@ -90,6 +94,8 @@ impl VersionedField {
&self,
container_version: &ContainerVersion,
) -> Option<TokenStream> {
let original_attributes = &self.original_attributes;

match &self.chain {
Some(chain) => {
// Check if the provided container version is present in the map
@@ -110,21 +116,25 @@ 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 {
ident: field_ident,
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,
})
}
27 changes: 25 additions & 2 deletions crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs
Original file line number Diff line number Diff line change
@@ -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<DataStruct, VersionedField> for VersionedStruct {
fn new(ident: Ident, data: DataStruct, attributes: ContainerAttributes) -> syn::Result<Self> {
fn new(
ident: Ident,
data: DataStruct,
attributes: ContainerAttributes,
original_attributes: Vec<Attribute>,
) -> syn::Result<Self> {
// Convert the raw version attributes into a container version.
let versions: Vec<_> = (&attributes).into();

@@ -77,6 +82,7 @@ impl Container<DataStruct, VersionedField> 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
}
106 changes: 106 additions & 0 deletions crates/stackable-versioned-macros/tests/attributes.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
6 changes: 6 additions & 0 deletions crates/stackable-versioned/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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