Skip to content
4 changes: 4 additions & 0 deletions crates/stackable-operator-derive/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

- Pass through struct and field comments and attributes. Add attribute for version specific docs. ([#816])

[#816]: https://github.com/stackabletech/operator-rs/pull/816

## [0.3.0] - 2024-05-08

### Changed
Expand Down
1 change: 1 addition & 0 deletions crates/stackable-versioned-macros/src/attrs/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ 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.
Expand Down
24 changes: 23 additions & 1 deletion crates/stackable-versioned-macros/src/attrs/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use crate::{attrs::container::ContainerAttributes, consts::DEPRECATED_PREFIX};
#[derive(Debug, FromField)]
#[darling(
attributes(versioned),
forward_attrs(allow, doc, cfg, serde),
forward_attrs,
and_then = FieldAttributes::validate
)]
pub(crate) struct FieldAttributes {
Expand All @@ -36,6 +36,8 @@ pub(crate) struct FieldAttributes {
pub(crate) renames: Vec<RenamedAttributes>,

pub(crate) deprecated: Option<DeprecatedAttributes>,

pub(crate) attrs: Vec<syn::Attribute>,
}

#[derive(Clone, Debug, FromMeta)]
Expand Down Expand Up @@ -79,6 +81,7 @@ impl FieldAttributes {
errors.handle(self.validate_action_combinations());
errors.handle(self.validate_action_order());
errors.handle(self.validate_field_name());
errors.handle(self.validate_field_attributes());

// Code quality validation
errors.handle(self.validate_deprecated_options());
Expand Down Expand Up @@ -207,6 +210,25 @@ impl FieldAttributes {
Ok(())
}

/// This associated function is called by the top-level validation function
/// and validates that disallowed field attributes are not used.
///
/// The following naming rules apply:
///
/// - `deprecated` must not be set on fields. Instead, the Versioned
/// method of deprecating fields should be used.
fn validate_field_attributes(&self) -> Result<(), Error> {
for attr in &self.attrs {
for segment in &attr.path().segments {
if segment.ident == "deprecated" {
return Err(Error::custom("field deprecation must be done using #[versioned(deprecated(since = \"VERSION\"))]")
.with_span(&segment.ident.span()));
}
}
}
Ok(())
}

fn validate_deprecated_options(&self) -> Result<(), Error> {
// TODO (@Techassi): Make the field 'note' optional, because in the
// future, the macro will generate parts of the deprecation note
Expand Down
13 changes: 12 additions & 1 deletion crates/stackable-versioned-macros/src/gen/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use darling::Error;
use k8s_version::Version;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Field, Ident, Path};
use syn::{Attribute, Field, Ident, Path};

use crate::{
attrs::field::FieldAttributes,
Expand All @@ -22,6 +22,7 @@ use crate::{
pub(crate) struct VersionedField {
chain: Option<BTreeMap<Version, FieldStatus>>,
inner: Field,
attrs: Vec<Attribute>,
}

impl VersionedField {
Expand Down Expand Up @@ -91,6 +92,7 @@ impl VersionedField {
Ok(Self {
chain: Some(actions),
inner: field,
attrs: attrs.attrs,
})
} else if !attrs.renames.is_empty() {
let mut actions = BTreeMap::new();
Expand Down Expand Up @@ -123,6 +125,7 @@ impl VersionedField {
Ok(Self {
chain: Some(actions),
inner: field,
attrs: attrs.attrs,
})
} else {
if let Some(added) = attrs.added {
Expand All @@ -139,12 +142,14 @@ impl VersionedField {
return Ok(Self {
chain: Some(actions),
inner: field,
attrs: attrs.attrs,
});
}

Ok(Self {
chain: None,
inner: field,
attrs: attrs.attrs,
})
}
}
Expand Down Expand Up @@ -211,6 +216,7 @@ impl VersionedField {
&self,
container_version: &ContainerVersion,
) -> Option<TokenStream> {
let attrs = &self.attrs;
match &self.chain {
Some(chain) => {
// Check if the provided container version is present in the map
Expand All @@ -228,21 +234,25 @@ impl VersionedField {
.expect("internal error: chain must contain container version")
{
FieldStatus::Added { ident, .. } => Some(quote! {
#(#attrs)*
pub #ident: #field_type,
}),
FieldStatus::Renamed { from: _, to } => Some(quote! {
#(#attrs)*
pub #to: #field_type,
}),
FieldStatus::Deprecated {
ident: field_ident,
note,
..
} => Some(quote! {
#(#attrs)*
#[deprecated = #note]
pub #field_ident: #field_type,
}),
FieldStatus::NotPresent => None,
FieldStatus::NoChange(field_ident) => Some(quote! {
#(#attrs)*
pub #field_ident: #field_type,
}),
}
Expand All @@ -255,6 +265,7 @@ impl VersionedField {
let field_type = &self.inner.ty;

Some(quote! {
#(#attrs)*
pub #field_ident: #field_type,
})
}
Expand Down
4 changes: 3 additions & 1 deletion crates/stackable-versioned-macros/src/gen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ pub(crate) mod vstruct;

pub(crate) fn expand(attrs: ContainerAttributes, input: DeriveInput) -> Result<TokenStream> {
let expanded = match input.data {
Data::Struct(data) => VersionedStruct::new(input.ident, data, attrs)?.generate_tokens(),
Data::Struct(data) => {
VersionedStruct::new(input.ident, data, attrs, input.attrs)?.generate_tokens()
}
_ => {
return Err(Error::new(
input.span(),
Expand Down
1 change: 1 addition & 0 deletions crates/stackable-versioned-macros/src/gen/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ pub(crate) struct ContainerVersion {
pub(crate) skip_from: bool,
pub(crate) inner: Version,
pub(crate) ident: Ident,
pub(crate) doc: Option<String>,
}
20 changes: 19 additions & 1 deletion crates/stackable-versioned-macros/src/gen/vstruct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use darling::FromField;
use itertools::Itertools;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{DataStruct, Error, Ident, Result};
use syn::{Attribute, DataStruct, Error, Ident, Result};

use crate::{
attrs::{container::ContainerAttributes, field::FieldAttributes},
Expand Down Expand Up @@ -30,13 +30,17 @@ pub(crate) struct VersionedStruct {
pub(crate) fields: Vec<VersionedField>,

pub(crate) skip_from: bool,

/// The original attributes that were added to the struct.
pub(crate) original_attrs: Vec<Attribute>,
}

impl VersionedStruct {
pub(crate) fn new(
ident: Ident,
data: DataStruct,
attributes: ContainerAttributes,
original_attrs: Vec<Attribute>,
) -> Result<Self> {
// Convert the raw version attributes into a container version.
let versions = attributes
Expand All @@ -46,6 +50,7 @@ impl VersionedStruct {
skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()),
ident: format_ident!("{version}", version = v.name.to_string()),
deprecated: v.deprecated.is_present(),
doc: v.doc.clone(),
inner: v.name,
})
.collect();
Expand Down Expand Up @@ -98,6 +103,7 @@ impl VersionedStruct {
versions,
fields,
ident,
original_attrs,
})
}

Expand Down Expand Up @@ -135,12 +141,24 @@ impl VersionedStruct {

let deprecated_attr = version.deprecated.then_some(quote! {#[deprecated]});
let module_name = &version.ident;
let attrs = &self.original_attrs;
let doc = if let Some(doc) = &version.doc {
let doc = format!("Docs for `{module_name}`: {doc}");
Some(quote! {
#[doc = ""]
#[doc = #doc]
})
} else {
None
};

// Generate tokens for the module and the contained struct
token_stream.extend(quote! {
#[automatically_derived]
#deprecated_attr
pub mod #module_name {
#(#attrs)*
#doc
pub struct #struct_name {
#fields
}
Expand Down
50 changes: 50 additions & 0 deletions crates/stackable-versioned-macros/tests/attributes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use stackable_versioned_macros::versioned;

#[ignore]
#[test]
fn pass_container_attributes() {
/// General docs that cover all versions
#[versioned(
version(name = "v1alpha1"),
version(
name = "v1beta1",
doc = r#"
Additional docs for this version. \
Supports multi-line docs.
"#
)
)]
// FIXME(@NickLarsenNZ): Derives
// #[derive(Default)]
struct Foo {
/// Always here
foo: String,

/// This is for bar (now deprecated)
#[versioned(deprecated(since = "v1beta1", note = "gone"))]
deprecated_bar: String,

/// This is for baz
#[versioned(added(since = "v1beta1"))]
// #[deprecated]
baz: String,

/// This is for qaax (previously qoox)
#[versioned(renamed(since = "v1beta1", from = "qoox"))]
qaax: 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"),
};
}
1 change: 1 addition & 0 deletions crates/stackable-versioned-macros/tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ struct Foo {
baz: bool,
}

#[ignore]
#[test]
fn basic() {
let _ = v1alpha1::Foo { jjj: 0, baz: false };
Expand Down
1 change: 1 addition & 0 deletions crates/stackable-versioned-macros/tests/deprecate.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use stackable_versioned_macros::versioned;

#[ignore]
#[test]
fn deprecate() {
#[versioned(
Expand Down
2 changes: 2 additions & 0 deletions crates/stackable-versioned-macros/tests/from.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ fn from_custom_default_fn() {
assert!(foo_v1beta1.baz);
}

#[ignore]
#[test]
fn skip_from_all() {
#[versioned(
Expand All @@ -70,6 +71,7 @@ fn skip_from_all() {
}
}

#[ignore]
#[test]
fn skip_from_version() {
#[versioned(
Expand Down
1 change: 1 addition & 0 deletions crates/stackable-versioned-macros/tests/rename.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use stackable_versioned_macros::versioned;

#[ignore]
#[test]
fn rename() {
#[versioned(
Expand Down