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
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
57 changes: 51 additions & 6 deletions crates/stackable-versioned-macros/src/codegen/item/variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::{
Direction, VersionDefinition,
changes::{BTreeMapExt, ChangesetExt},
item::{ItemStatus, generate_attributes},
module::ModuleGenerationContext,
},
utils::ItemIdents,
};
Expand All @@ -24,12 +25,18 @@ pub struct VersionedVariant {
pub changes: Option<BTreeMap<Version, ItemStatus>>,
pub idents: VariantIdents,
pub fields: Fields,
pub nested: bool,
}

impl VersionedVariant {
pub fn new(variant: Variant, versions: &[VersionDefinition]) -> Result<Self> {
pub fn new(
variant: Variant,
versions: &[VersionDefinition],
experimental_conversion_tracking: bool,
) -> Result<Self> {
let variant_attributes = VariantAttributes::from_variant(&variant)?;
variant_attributes.validate_versions(versions)?;
variant_attributes.validate_nested_flag(experimental_conversion_tracking)?;

let idents = VariantIdents::from(variant.ident);

Expand All @@ -41,6 +48,7 @@ impl VersionedVariant {
bang_token: Not([Span::call_site()]),
});
let previous_docs = variant_attributes.common.previous_docs();
let nested = variant_attributes.nested.is_present();
let changes = variant_attributes.common.into_changeset(&idents, ty);

Ok(Self {
Expand All @@ -49,6 +57,7 @@ impl VersionedVariant {
fields: variant.fields,
idents,
changes,
nested,
})
}

Expand Down Expand Up @@ -152,9 +161,10 @@ impl VersionedVariant {
version: &VersionDefinition,
next_version: &VersionDefinition,
enum_ident: &IdentString,
mod_gen_ctx: ModuleGenerationContext<'_>,
) -> Option<TokenStream> {
let from_fields = self.generate_from_fields();
let for_fields = self.generate_for_fields();
let for_fields = self.generate_for_fields(mod_gen_ctx);

#[allow(clippy::single_match_else)]
match &self.changes {
Expand Down Expand Up @@ -203,20 +213,55 @@ impl VersionedVariant {
}
}

fn generate_for_fields(&self) -> Option<TokenStream> {
fn generate_for_fields(&self, mod_gen_ctx: ModuleGenerationContext<'_>) -> Option<TokenStream> {
match &self.fields {
Fields::Named(fields_named) => {
let fields = Self::named_field_idents(fields_named);
Some(quote! { { #(#fields: #fields.into(),)* } })
let conversions = fields.iter().map(|field| {
self.generate_conversion_function(Some(&field.to_string()), mod_gen_ctx)
});

Some(quote! { { #(#fields: #fields.#conversions,)* } })
}
Fields::Unnamed(fields_unnamed) => {
let fields = Self::unnamed_field_ident(fields_unnamed);
Some(quote! { ( #(#fields.into())* ) })

// Newtype variants (which are the most common variants with data) don't need an
// additional path segment, as the variant only contains a single field.
let conversions = (0..fields.len()).map(|index| {
let child = (fields.len() > 1).then(|| index.to_string());
self.generate_conversion_function(child.as_deref(), mod_gen_ctx)
});

Some(quote! { ( #(#fields.#conversions),* ) })
}
Fields::Unit => None,
}
}

/// Generates the conversion function for a single field of the variant data.
///
/// The data of variants marked as nested is converted with support for tracking. The path
/// passed down consists of the variant name and the provided `child`, if any.
fn generate_conversion_function(
&self,
child: Option<&str>,
mod_gen_ctx: ModuleGenerationContext<'_>,
) -> TokenStream {
if !self.nested {
return quote! { into() };
}

let versioned_path = &*mod_gen_ctx.crates.versioned;
let variant = &self.idents.original;
let child_string = match child {
Some(child) => format!("{variant}.{child}"),
None => variant.to_string(),
};

quote! { tracking_into(status, &#versioned_path::jthong_path(parent, #child_string)) }
}

fn generate_from_fields(&self) -> Option<TokenStream> {
match &self.fields {
Fields::Named(fields_named) => {
Expand All @@ -225,7 +270,7 @@ impl VersionedVariant {
}
Fields::Unnamed(fields_unnamed) => {
let fields = Self::unnamed_field_ident(fields_unnamed);
Some(quote! { ( #(#fields)* ) })
Some(quote! { ( #(#fields),* ) })
}
Fields::Unit => None,
}
Expand Down
20 changes: 11 additions & 9 deletions crates/stackable-versioned-macros/src/codegen/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,24 @@ impl Module {
let mut submodules = HashMap::new();
let mut containers = Vec::new();

let experimental_conversion_tracking = module_attributes
.options
.kubernetes
.experimental_conversion_tracking
.is_present();

for item in items {
match item {
Item::Enum(item_enum) => {
if let Some(container) =
errors.handle(Container::new_enum(item_enum, &versions))
{
if let Some(container) = errors.handle(Container::new_enum(
item_enum,
&versions,
experimental_conversion_tracking,
)) {
containers.push(container);
}
}
Item::Struct(item_struct) => {
let experimental_conversion_tracking = module_attributes
.options
.kubernetes
.experimental_conversion_tracking
.is_present();

if let Some(container) = errors.handle(Container::new_struct(
item_struct,
&versions,
Expand Down
44 changes: 44 additions & 0 deletions crates/stackable-versioned-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,50 @@ mod utils;
/// # fn main() {}
/// ```
///
/// Changes can also be tracked through enums. With conversion tracking enabled,
/// versioned structs and enums only implement `TrackingFrom` instead of `From`.
/// As such, every field and every enum variant containing a versioned struct
/// or enum needs to be marked with `#[versioned(nested)]`. All data of a nested
/// variant is converted with tracking support. The path of tracked values
/// contains the variant name, eg. `$.connector.Baz.quox`.
///
/// ```
/// # use stackable_versioned_macros::versioned;
/// # use kube::CustomResource;
/// # use schemars::JsonSchema;
/// # use serde::{Deserialize, Serialize};
/// #[versioned(
/// version(name = "v1alpha1"),
/// version(name = "v1beta1"),
/// options(k8s(experimental_conversion_tracking))
/// )]
/// mod versioned {
/// #[versioned(crd(group = "example.com", doc = "My custom resource."))]
/// #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)]
/// struct FooSpec {
/// #[versioned(nested)]
/// connector: Connector,
/// }
///
/// #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
/// enum Connector {
/// #[versioned(nested)]
/// Baz(Baz),
///
/// Unit,
/// }
///
/// #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
/// struct Baz {
/// quax: String,
///
/// #[versioned(added(since = "v1beta1"))]
/// quox: bool,
/// }
/// }
/// # fn main() {}
/// ```
///
/// # OpenTelemetry Semantic Conventions
///
/// If tracing is enabled, various traces and events are emitted. The fields of
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use stackable_versioned::versioned;
// ---
#[versioned(
version(name = "v1alpha1"),
version(name = "v1alpha2"),
options(k8s(experimental_conversion_tracking))
)]
// ---
pub(crate) mod versioned {
#[versioned(crd(group = "stackable.tech", doc = "Test"))]
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)]
pub(crate) struct FooSpec {
// With conversion tracking enabled, enums only implement TrackingFrom. As such, fields
// using a versioned enum need to be marked as nested.
#[versioned(nested)]
connector: Connector,
}

#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub(crate) enum Connector {
// The data of this variant is a versioned struct, which tracks changes.
#[versioned(nested)]
Iceberg(IcebergConnector),

// Nested variants with named fields are supported as well.
#[versioned(nested)]
Hive { connector: IcebergConnector },

// Nested tuple variants with multiple fields use the index as an additional path segment.
#[versioned(nested)]
Both(IcebergConnector, IcebergConnector),

// The data of this variant is not versioned and is converted using From.
Tpch(TpchConnector),

Tpcds(TpchConnector, TpchConnector),

Unit,
}

#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub(crate) struct IcebergConnector {
metastore: Option<String>,

#[versioned(added(since = "v1alpha2"))]
rest_catalog_uri: Option<String>,
}
}
// ---
fn main() {}

#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct TpchConnector {}
Loading
Loading