Skip to content

Commit 5ef8294

Browse files
sbernauerTechassi
andauthored
feat(versioned): Support conversion tracking through enums (#1294)
* feat(versioned): Support conversion tracking through enums With experimental_conversion_tracking enabled, versioned enums now implement TrackingFrom instead of From, in line with structs. Variants marked with as an additional path segment (eg. `$.connector.Iceberg.field`). Also fix the generated From impls for variants with multiple unnamed fields, which were missing separating commas. * refactor(versioned): Cleanup and improve code, leave TODO comments * fix: Bump rustls to 0.23.46 to negate RUSTSEC-2026-0346 and RUSTSEC-2026-0347 * test(versioned): Remove extra test, migrate into Person spec * docs(versioned): Trim down on duplicate information * chore(versioned): Remove superfluous/inaccurate dev comment * chore(versioned): Fix typos * chore(versioned): Update changelog entries * chore(versioned): Fix some more typos --------- Co-authored-by: Techassi <git@techassi.dev>
1 parent 433ac4f commit 5ef8294

19 files changed

Lines changed: 1111 additions & 58 deletions

‎Cargo.lock‎

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/stackable-versioned-macros/src/attrs/item/variant.rs‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use convert_case::{Case, Casing};
2-
use darling::{Error, FromVariant, Result};
2+
use darling::{Error, FromVariant, Result, util::Flag};
33
use syn::{Attribute, Ident};
44

55
use crate::{
@@ -40,6 +40,11 @@ pub struct VariantAttributes {
4040
// FromMeta.
4141
/// The original attributes for the field.
4242
pub attrs: Vec<Attribute>,
43+
44+
/// Indicates that the data of this variant contains nested sub structs or enums. The
45+
/// indicator is needed to let the macro know to generate conversion code with support for
46+
/// tracking across container boundaries.
47+
pub nested: Flag,
4348
}
4449

4550
impl VariantAttributes {
@@ -73,6 +78,17 @@ impl VariantAttributes {
7378
pub fn validate_versions(&self, versions: &[VersionDefinition]) -> Result<()> {
7479
self.common.validate_versions(versions)
7580
}
81+
82+
pub fn validate_nested_flag(&self, experimental_conversion_tracking: bool) -> Result<()> {
83+
if self.nested.is_present() && !experimental_conversion_tracking {
84+
return Err(
85+
Error::custom("the `nested` argument can only be used if the module-level `experimental_conversion_tracking` flag is set")
86+
.with_span(&self.nested.span())
87+
);
88+
}
89+
90+
Ok(())
91+
}
7692
}
7793

7894
// We don't want to take a dependency on the (unmaintained[1]) `convert_case_extras` crate just for

‎crates/stackable-versioned-macros/src/codegen/container/enum.rs‎

Lines changed: 84 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use std::ops::Not;
1+
use std::{cell::OnceCell, ops::Not};
22

33
use darling::{FromAttributes, Result};
44
use proc_macro2::TokenStream;
55
use quote::quote;
6-
use syn::{Generics, ItemEnum};
6+
use syn::{Generics, ItemEnum, parse_quote};
77

88
use crate::{
99
attrs::container::ContainerAttributes,
@@ -19,12 +19,17 @@ use crate::{
1919
};
2020

2121
impl Container {
22-
pub fn new_enum(item_enum: ItemEnum, versions: &[VersionDefinition]) -> Result<Self> {
22+
pub fn new_enum(
23+
item_enum: ItemEnum,
24+
versions: &[VersionDefinition],
25+
experimental_conversion_tracking: bool,
26+
) -> Result<Self> {
2327
let attributes = ContainerAttributes::from_attributes(&item_enum.attrs)?;
2428

2529
let mut versioned_variants = Vec::new();
2630
for variant in item_enum.variants {
27-
let mut versioned_variant = VersionedVariant::new(variant, versions)?;
31+
let mut versioned_variant =
32+
VersionedVariant::new(variant, versions, experimental_conversion_tracking)?;
2833
versioned_variant.insert_container_versions(versions);
2934
versioned_variants.push(versioned_variant);
3035
}
@@ -45,6 +50,7 @@ impl Container {
4550
};
4651

4752
Ok(Self::Enum(Enum {
53+
tracking_generics: OnceCell::new(),
4854
generics: item_enum.generics,
4955
variants: versioned_variants,
5056
common,
@@ -63,14 +69,25 @@ pub struct Enum {
6369

6470
/// Generic types of the enum
6571
pub generics: Generics,
72+
73+
/// Generic types of the enum which additionally also include the tracking state generic type
74+
/// and where clause.
75+
///
76+
/// We use a [`OnceCell`] here to avoid outer mutability and instead rely on interior mutability
77+
/// to lazily initialize this field. This is done to avoid the macro the re-clone and
78+
/// re-construct these generics over and over again.
79+
///
80+
/// SAFETY: Using [`OnceCell`] instead of `OnceLock` is fine here, because proc-macros run in a
81+
/// single thread.
82+
tracking_generics: OnceCell<Generics>,
6683
}
6784

6885
// Common token generation
6986
impl Enum {
7087
pub fn generate_tokens<'a>(
7188
&'a self,
7289
versions: &'a [VersionDefinition],
73-
gen_ctx: ModuleGenerationContext<'a>,
90+
mod_gen_ctx: ModuleGenerationContext<'a>,
7491
) -> ContainerTokens<'a> {
7592
let mut versions = versions.iter().peekable();
7693
let mut container_tokens = ContainerTokens::default();
@@ -80,8 +97,9 @@ impl Enum {
8097
let ver_ctx = VersionContext::new(version, next_version);
8198

8299
let enum_definition = self.generate_definition(ver_ctx);
83-
let upgrade_from = self.generate_from_impl(Direction::Upgrade, ver_ctx, gen_ctx);
84-
let downgrade_from = self.generate_from_impl(Direction::Downgrade, ver_ctx, gen_ctx);
100+
let upgrade_from = self.generate_from_impl(Direction::Upgrade, ver_ctx, mod_gen_ctx);
101+
let downgrade_from =
102+
self.generate_from_impl(Direction::Downgrade, ver_ctx, mod_gen_ctx);
85103

86104
container_tokens
87105
.extend_inner(&version.inner, enum_definition)
@@ -116,11 +134,11 @@ impl Enum {
116134
}
117135

118136
// TODO (@Techassi): Add doc comments
119-
pub fn generate_from_impl(
137+
fn generate_from_impl(
120138
&self,
121139
direction: Direction,
122140
ver_ctx: VersionContext<'_>,
123-
gen_ctx: ModuleGenerationContext<'_>,
141+
mod_gen_ctx: ModuleGenerationContext<'_>,
124142
) -> Option<TokenStream> {
125143
if ver_ctx.version.skip_from || self.common.options.skip_from {
126144
return None;
@@ -149,15 +167,15 @@ impl Enum {
149167

150168
// Only add the #[automatically_derived] attribute only if this impl is used
151169
// outside of a module (in standalone mode).
152-
let automatically_derived = gen_ctx.add_attributes
170+
let automatically_derived = mod_gen_ctx.add_attributes
153171
.not()
154172
.then(|| quote! {#[automatically_derived]});
155173

156174
let variants = |direction: Direction| -> TokenStream {
157175
self.variants
158176
.iter()
159177
.filter_map(|v| {
160-
v.generate_for_from_impl(direction, version, next_version, enum_ident)
178+
v.generate_for_from_impl(direction, version, next_version, enum_ident, mod_gen_ctx)
161179
})
162180
.collect()
163181
};
@@ -177,22 +195,68 @@ impl Enum {
177195
},
178196
};
179197

180-
quote! {
181-
#automatically_derived
182-
#allow_attribute
183-
impl #impl_generics ::core::convert::From<#from_module_ident::#enum_ident #type_generics> for #for_module_ident::#enum_ident #type_generics
184-
#where_clause
185-
{
186-
fn from(#from_enum_ident: #from_module_ident::#enum_ident #type_generics) -> Self {
187-
match #from_enum_ident {
188-
#variants
198+
// TODO (@Techassi): This currently does NOT support tracking changes to the enum itself,
199+
// only changes in nested structs are tracked.
200+
if mod_gen_ctx.kubernetes_options.experimental_conversion_tracking.is_present() {
201+
let versioned_path = &*mod_gen_ctx.crates.versioned;
202+
203+
let (impl_generics, _, where_clause) = self.get_tracking_generics(mod_gen_ctx).split_for_impl();
204+
205+
quote! {
206+
#automatically_derived
207+
#allow_attribute
208+
impl #impl_generics #versioned_path::TrackingFrom<#from_module_ident::#enum_ident #type_generics, S> for #for_module_ident::#enum_ident #type_generics
209+
#where_clause
210+
{
211+
fn tracking_from(#from_enum_ident: #from_module_ident::#enum_ident #type_generics, status: &mut S, parent: &str) -> Self {
212+
// TODO (@Techassi): Only emit this if any of the variants below need it
213+
use #versioned_path::TrackingInto as _;
214+
215+
match #from_enum_ident {
216+
#variants
217+
}
218+
}
219+
}
220+
}
221+
} else {
222+
quote! {
223+
#automatically_derived
224+
#allow_attribute
225+
impl #impl_generics ::core::convert::From<#from_module_ident::#enum_ident #type_generics> for #for_module_ident::#enum_ident #type_generics
226+
#where_clause
227+
{
228+
fn from(#from_enum_ident: #from_module_ident::#enum_ident #type_generics) -> Self {
229+
match #from_enum_ident {
230+
#variants
231+
}
189232
}
190233
}
191234
}
192235
}
193236
})
194237
}
195238

239+
/// Construct (if needed) and return the [`Enum`]'s generics extended with the tracking status
240+
/// generic type and where clause.
241+
///
242+
/// This function lazily initializes the underlying field in [`Enum`]. Afterwards, the value is
243+
/// re-used without constructing the value from scratch over and over again. This is achieved
244+
/// through interior mutability via [`OnceCell`]. See [`Enum`]'s `tracking_generics` field for
245+
/// more details.
246+
pub fn get_tracking_generics(&self, mod_gen_ctx: ModuleGenerationContext<'_>) -> &Generics {
247+
self.tracking_generics.get_or_init(|| {
248+
let versioned_path = &*mod_gen_ctx.crates.versioned;
249+
250+
let mut tracking_generics = self.generics.clone();
251+
tracking_generics.params.push(parse_quote! { S });
252+
tracking_generics.make_where_clause().predicates.push(
253+
parse_quote! { S: #versioned_path::TrackingStatus + ::core::default::Default },
254+
);
255+
256+
tracking_generics
257+
})
258+
}
259+
196260
/// Returns whether any variant is deprecated in the provided `version`.
197261
fn is_any_variant_deprecated(&self, version: &VersionDefinition) -> bool {
198262
// First, iterate over all variants. The `any` function will return true

‎crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,8 @@ impl Struct {
478478
) -> TokenStream {
479479
// TODO (@Techassi): A bunch this stuff is duplicated in self.generate_tracking_from_impl.
480480
// Ideally we remove that duplication.
481+
// The enum's codegen code removes a whole bunch of the duplication. Implement something
482+
// similar here.
481483
let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl();
482484

483485
let from_struct_ident = &self.common.idents.parameter;

‎crates/stackable-versioned-macros/src/codegen/item/field.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,10 @@ impl VersionedField {
426426
custom_conversion_function: Option<&Path>,
427427
) -> TokenStream {
428428
match custom_conversion_function {
429+
// TODO (@Techassi): We should also support tracking here. Currently, the nested hint is
430+
// ignored and the generated code doesn't force the custom conversion function to accept
431+
// a tracking status.
432+
//
429433
// The user specified a custom conversion function which will be used here instead of the
430434
// default conversion call which utilizes From impls.
431435
Some(convert_fn) => quote! {

0 commit comments

Comments
 (0)