Skip to content
Open
Show file tree
Hide file tree
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
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 20 additions & 3 deletions crates/stackable-versioned-macros/src/attrs/item/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use syn::{Attribute, Path, Type, spanned::Spanned};

use crate::{
codegen::{VersionDefinition, item::ItemStatus},
utils::ItemIdents,
utils::{ItemIdents, doc_comments::DocComments as _},
};

mod field;
Expand Down Expand Up @@ -223,9 +223,12 @@ impl CommonItemAttributes {
let mut errors = Error::accumulator();

for change in &self.changes {
if change.from_name.is_none() && change.from_type.is_none() {
if change.from_name.is_none()
&& change.from_type.is_none()
&& change.from_docs.is_none()
{
errors.push(Error::custom(
"both `from_name` and `from_type` are unset. Is this `changed()` action needed?"
"`from_name`, `from_type` and `from_docs` are unset. Is this `changed()` action needed?"
).with_span(&change.since.span()));
}

Expand Down Expand Up @@ -288,6 +291,18 @@ impl CommonItemAttributes {
}

impl CommonItemAttributes {
/// Returns the doc comments of the item before each change which provides `from_docs`, keyed
/// by the version of the change.
pub fn previous_docs(&self) -> BTreeMap<Version, Vec<String>> {
self.changes
.iter()
.filter_map(|change| {
let docs = change.from_docs.as_deref()?;
Some((*change.since, docs.as_str().into_doc_comments()))
})
.collect()
}

#[expect(clippy::too_many_lines)]
pub fn into_changeset(
self,
Expand Down Expand Up @@ -458,11 +473,13 @@ fn default_default_fn() -> SpannedValue<Path> {
/// - `changed(since = "...", from_name = "...", from_type="...")`
/// - `changed(since = "...", from_name = "...", from_type="...", upgrade_with = "...")`
/// - `changed(since = "...", from_name = "...", from_type="...", downgrade_with = "...")`
/// - `changed(since = "...", from_docs = "...")`
#[derive(Clone, Debug, FromMeta)]
pub struct ChangedAttributes {
pub since: SpannedValue<Version>,
pub from_name: Option<SpannedValue<String>>,
pub from_type: Option<SpannedValue<Type>>,
pub from_docs: Option<SpannedValue<String>>,
pub upgrade_with: Option<SpannedValue<Path>>,
pub downgrade_with: Option<SpannedValue<Path>>,
}
Expand Down
18 changes: 17 additions & 1 deletion crates/stackable-versioned-macros/src/attrs/item/variant.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use convert_case::{Case, Casing};
use darling::{Error, FromVariant, Result};
use darling::{Error, FromVariant, Result, util::Flag};
use syn::{Attribute, Ident};

use crate::{
Expand Down Expand Up @@ -40,6 +40,11 @@ pub struct VariantAttributes {
// FromMeta.
/// The original attributes for the field.
pub attrs: Vec<Attribute>,

/// Indicates that the data of this variant contains nested sub structs or enums. The
/// indicator is needed to let the macro know to generate conversion code with support for
/// tracking across container boundaries.
pub nested: Flag,
}

impl VariantAttributes {
Expand Down Expand Up @@ -73,6 +78,17 @@ impl VariantAttributes {
pub fn validate_versions(&self, versions: &[VersionDefinition]) -> Result<()> {
self.common.validate_versions(versions)
}

pub fn validate_nested_flag(&self, experimental_conversion_tracking: bool) -> Result<()> {
if self.nested.is_present() && !experimental_conversion_tracking {
return Err(
Error::custom("the `nested` argument can only be used if the module-level `experimental_conversion_tracking` flag is set")
.with_span(&self.nested.span())
);
}

Ok(())
}
}

// We don't want to take a dependency on the (unmaintained[1]) `convert_case_extras` crate just for
Expand Down
45 changes: 41 additions & 4 deletions crates/stackable-versioned-macros/src/codegen/container/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::ops::Not;
use darling::{FromAttributes, Result};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Generics, ItemEnum};
use syn::{Generics, ItemEnum, parse_quote};

use crate::{
attrs::container::ContainerAttributes,
Expand All @@ -19,12 +19,17 @@ use crate::{
};

impl Container {
pub fn new_enum(item_enum: ItemEnum, versions: &[VersionDefinition]) -> Result<Self> {
pub fn new_enum(
item_enum: ItemEnum,
versions: &[VersionDefinition],
experimental_conversion_tracking: bool,
) -> Result<Self> {
let attributes = ContainerAttributes::from_attributes(&item_enum.attrs)?;

let mut versioned_variants = Vec::new();
for variant in item_enum.variants {
let mut versioned_variant = VersionedVariant::new(variant, versions)?;
let mut versioned_variant =
VersionedVariant::new(variant, versions, experimental_conversion_tracking)?;
versioned_variant.insert_container_versions(versions);
versioned_variants.push(versioned_variant);
}
Expand Down Expand Up @@ -157,7 +162,7 @@ impl Enum {
self.variants
.iter()
.filter_map(|v| {
v.generate_for_from_impl(direction, version, next_version, enum_ident)
v.generate_for_from_impl(direction, version, next_version, enum_ident, gen_ctx)
})
.collect()
};
Expand All @@ -177,6 +182,38 @@ impl Enum {
},
};

// With conversion tracking enabled, the enum needs to forward the status and the
// current path to the data of nested variants. As such, only a TrackingFrom impl is
// generated, which is in line with the impls generated for structs.
if gen_ctx.kubernetes_options.experimental_conversion_tracking.is_present() {
let versioned_path = &*gen_ctx.crates.versioned;

let mut tracking_generics = self.generics.clone();
tracking_generics.params.push(parse_quote! { S });
tracking_generics
.make_where_clause()
.predicates
.push(parse_quote! { S: #versioned_path::TrackingStatus + ::core::default::Default });
let (impl_generics, _, where_clause) = tracking_generics.split_for_impl();

return quote! {
#automatically_derived
#allow_attribute
impl #impl_generics #versioned_path::TrackingFrom<#from_module_ident::#enum_ident #type_generics, S> for #for_module_ident::#enum_ident #type_generics
#where_clause
{
fn tracking_from(#from_enum_ident: #from_module_ident::#enum_ident #type_generics, status: &mut S, parent: &str) -> Self {
// TODO (@Techassi): Only emit this if any of the variants below need it
use #versioned_path::TrackingInto as _;

match #from_enum_ident {
#variants
}
}
}
};
}

quote! {
#automatically_derived
#allow_attribute
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,11 @@ impl Struct {
// Ideally we would integrate with the serde(rename) functionality to produce these field
// names.
let inserts = self.generate_tracking_inserts(direction, next_version, mod_gen_ctx);
let post_inserts =
self.generate_tracking_post_inserts(direction, next_version, mod_gen_ctx);
let removals = self.generate_tracking_removals(direction, next_version, mod_gen_ctx);
let json_paths = self.generate_json_paths(next_version, mod_gen_ctx);
let tracked_values = self.generate_tracked_values(direction, next_version, mod_gen_ctx);

// TODO (@Techassi): Re-add support for generics
// TODO (@Techassi): We know the status, so we can hard-code it, but hard to track across structs
Expand All @@ -192,10 +195,18 @@ impl Struct {
// the upgrade or downgrade section. Only then we can convert the spec.
#inserts

// Fields which changed their type are consumed by the conversion, so their
// values need to be serialized before converting the spec.
#tracked_values

let mut spec = Self {
#fields
};

// Fields which changed their type can only be inserted into the status after
// the spec is converted, because the downgraded value is tracked as well.
#post_inserts

// After the spec is converted, depending on the direction, we need to apply
// changed values from either the upgrade or downgrade section. Afterwards
// we can return the successfully converted spec and the status contains
Expand Down Expand Up @@ -237,6 +248,12 @@ impl Struct {
})
.collect();

// This is the case if only fields which changed their type need to be tracked.
// These are inserted after the conversion, see generate_tracking_post_inserts.
if inserts.is_empty() {
return None;
}

Some(quote! {
let upgrades = status
.changes()
Expand All @@ -250,6 +267,42 @@ impl Struct {
}
}

fn generate_tracking_post_inserts(
&self,
direction: Direction,
next_version: &VersionDefinition,
mod_gen_ctx: ModuleGenerationContext<'_>,
) -> Option<TokenStream> {
match direction {
Direction::Upgrade => None,
Direction::Downgrade => {
let next_version_string = next_version.inner.to_string();

let post_inserts: TokenStream = self
.fields
.iter()
.filter_map(|f| {
f.generate_for_status_post_insertion(direction, next_version, mod_gen_ctx)
})
.collect();

if post_inserts.is_empty() {
return None;
}

Some(quote! {
let upgrades = status
.changes()
.upgrades
.entry(#next_version_string.to_owned())
.or_default();

#post_inserts
})
}
}
}

fn generate_tracking_removals(
&self,
direction: Direction,
Expand All @@ -263,18 +316,32 @@ impl Struct {
let match_arms: TokenStream = self
.fields
.iter()
.filter_map(|f| f.generate_for_status_removal(direction, next_version))
.filter_map(|f| f.generate_for_status_removal(direction, next_version, mod_gen_ctx))
.collect();

match direction {
Direction::Upgrade => {
let next_version_string = next_version.inner.to_string();
let versioned_path = &*mod_gen_ctx.crates.versioned;

// The downgraded value is only needed by fields which changed their type. Binding
// it unconditionally would result in an unused variable otherwise.
let has_type_changes = self.fields.iter().any(|f| {
f.changes.as_ref().is_some_and(|c| {
c.value_is(&next_version.inner, ItemStatus::is_type_change)
})
});

let downgraded_value = if has_type_changes {
quote! { downgraded_value }
} else {
quote! { .. }
};

Some(quote! {
// NOTE (@Techassi): This is an awkward thing to do. Can we possibly use &str for the keys here?
if let Some(upgrades) = status.changes().upgrades.remove(&#next_version_string.to_owned()) {
for #versioned_path::ChangedValue { json_path, value } in upgrades {
for #versioned_path::ChangedValue { json_path, value, #downgraded_value } in upgrades {
match json_path {
#match_arms
_ => unreachable!(),
Expand All @@ -299,17 +366,37 @@ impl Struct {
.collect()
}

fn generate_tracked_values(
&self,
direction: Direction,
next_version: &VersionDefinition,
mod_gen_ctx: ModuleGenerationContext<'_>,
) -> TokenStream {
let from_struct_ident = &self.common.idents.parameter;

self.fields
.iter()
.filter_map(|f| {
f.generate_for_tracked_value(
direction,
next_version,
from_struct_ident,
mod_gen_ctx,
)
})
.collect()
}

pub(super) fn needs_tracking(&self, version: &VersionDefinition) -> bool {
self.fields.iter().any(|f| {
f.changes.as_ref().is_some_and(|c| {
c.value_is(&version.inner, |s| {
// For now, only added fields need to be tracked. In the future, removals and
// type changes also need to be tracked
// For now, only added fields and fields which changed their type need to be
// tracked. In the future, removals also need to be tracked.
match s {
ItemStatus::Addition { .. } => true,
// TODO (@Techassi): Support tracking for changed fields
ItemStatus::Change { .. }
| ItemStatus::Deprecation { .. }
ItemStatus::Change { .. } => s.is_type_change(),
ItemStatus::Deprecation { .. }
| ItemStatus::NoChange { .. }
| ItemStatus::NotPresent => false,
}
Expand Down
Loading
Loading