Skip to content
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

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

Merged
merged 9 commits into from
Aug 22, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
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,
Expand All @@ -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,
Expand All @@ -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()
Expand All @@ -55,7 +55,7 @@ where
}
}

for rename in &*self.common_attrs().renames {
for rename in &*self.common_attributes().renames {
if !container_attrs
.versions
.iter()
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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()?;

Expand All @@ -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(
Expand All @@ -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))
}
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
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};

Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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)
}
Expand Down
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};

Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
Expand Down
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};

Expand All @@ -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.
///
Expand All @@ -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>,
}
Loading
Loading