Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 12 additions & 1 deletion crates/stackable-versioned-macros/src/attrs/item/field.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use darling::{Error, FromField, Result, util::Flag};
use darling::{Error, FromField, FromMeta, Result, util::Flag};
use syn::{Attribute, Ident};

use crate::{
Expand Down Expand Up @@ -44,6 +44,10 @@ pub struct FieldAttributes {
/// is needed to let the macro know to generate conversion code with support
/// for tracking across struct boundaries.
pub nested: Flag,

/// Provide a hint if a field is wrapped in either `Option` or `Vec` to
/// generate correct code in the `From` impl blocks.
pub hint: Option<Hint>,
}

impl FieldAttributes {
Expand Down Expand Up @@ -81,3 +85,10 @@ impl FieldAttributes {
Ok(())
}
}

/// Supported field hints.
#[derive(Debug, FromMeta)]
pub enum Hint {
Option,
Vec,
}
250 changes: 153 additions & 97 deletions crates/stackable-versioned-macros/src/codegen/item/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ use darling::{FromField, Result, util::IdentString};
use k8s_version::Version;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, Field, Ident, Type};
use syn::{Attribute, Field, Ident, Path, Type};

use crate::{
attrs::item::FieldAttributes,
attrs::item::{FieldAttributes, Hint},
codegen::{
Direction, VersionDefinition,
changes::{BTreeMapExt, ChangesetExt},
Expand All @@ -21,6 +21,7 @@ pub struct VersionedField {
pub original_attributes: Vec<Attribute>,
pub changes: Option<BTreeMap<Version, ItemStatus>>,
pub idents: FieldIdents,
pub hint: Option<Hint>,
pub nested: bool,
pub ty: Type,
}
Expand All @@ -47,6 +48,7 @@ impl VersionedField {

Ok(Self {
original_attributes: field_attributes.attrs,
hint: field_attributes.hint,
ty: field.ty,
changes,
idents,
Expand All @@ -60,19 +62,31 @@ impl VersionedField {
}
}

/// Generates field definitions for the use inside container (struct) definitions.
///
/// This function needs to take into account multiple conditions:
///
/// - Only emit the field if it exists for the currently generated version.
/// - Emit field with new name and type if there was a name and/or type change.
/// - Handle deprecated fields accordingly.
///
/// ### Example
///
/// ```ignore
/// struct Foo {
/// bar: usize, // < This functions generates one or more of these definitions
/// }
/// ```
pub fn generate_for_container(&self, version: &VersionDefinition) -> Option<TokenStream> {
let original_attributes = &self.original_attributes;

match &self.changes {
Some(changes) => {
// Check if the provided container version is present in the map
// of actions. If it is, some action occurred in exactly that
// version and thus code is generated for that field based on
// the type of action.
// If not, the provided version has no action attached to it.
// The code generation then depends on the relation to other
// versions (with actions).

// Check if the provided container version is present in the map of actions. If it
// is, some action occurred in exactly that version and thus code is generated for
// that field based on the type of action.
// If not, the provided version has no action attached to it. The code generation
// then depends on the relation to other versions (with actions).
let field_type = &self.ty;

// NOTE (@Techassi): https://rust-lang.github.io/rust-clippy/master/index.html#/expect_fun_call
Expand All @@ -97,14 +111,12 @@ impl VersionedField {
note,
..
} => {
// FIXME (@Techassi): Emitting the deprecated attribute
// should cary over even when the item status is
// 'NoChange'.
// TODO (@Techassi): Make the generation of deprecated
// items customizable. When a container is used as a K8s
// CRD, the item must continue to exist, even when
// deprecated. For other versioning use-cases, that
// might not be the case.
// FIXME (@Techassi): Emitting the deprecated attribute should cary over even
// when the item status is 'NoChange'.
// TODO (@Techassi): Make the generation of deprecated items customizable.
// When a container is used as a K8s CRD, the item must continue to exist,
// even when deprecated. For other versioning use-cases, that might not be
// the case.
let deprecated_attr = if let Some(note) = note {
quote! {#[deprecated = #note]}
} else {
Expand All @@ -124,8 +136,7 @@ impl VersionedField {
ty,
..
} => {
// TODO (@Techassi): Also carry along the deprecation
// note.
// TODO (@Techassi): Also carry along the deprecation note.
let deprecated_attr = previously_deprecated.then(|| quote! {#[deprecated]});

Some(quote! {
Expand All @@ -137,8 +148,8 @@ impl VersionedField {
}
}
None => {
// If there is no chain of field actions, the field is not
// versioned and therefore included in all versions.
// If there is no chain of field actions, the field is not versioned and therefore
// included in all versions.
let field_ident = &self.idents.original;
let field_type = &self.ty;

Expand All @@ -150,6 +161,27 @@ impl VersionedField {
}
}

/// Generates field definitions for the use inside `From` impl blocks.
///
/// This function needs to take into account multiple conditions:
///
/// - Only emit the field if it exists for the currently generated version.
/// - Emit fields which previously didn't exist with the correct initialization function.
/// - Emit field with new name and type if there was a name and/or type change.
/// - Handle tracking conversions without data-loss.
/// - Handle deprecated fields accordingly.
///
/// ### Example
///
/// ```ignore
/// impl From<v1alpha1::Foo> for v1alpha2::Foo {
/// fn from(value: v1alpha1::Foo) -> Self {
/// Self {
/// bar: value.bar, // < This functions generates one or more of these definitions
/// }
/// }
/// }
/// ```
pub fn generate_for_from_impl(
&self,
direction: Direction,
Expand All @@ -163,9 +195,9 @@ impl VersionedField {
let change = changes.get_expect(&version.inner);

match (change, next_change) {
// If both this status and the next one is NotPresent, which means
// a field was introduced after a bunch of versions, we don't
// need to generate any code for the From impl.
// If both this status and the next one is NotPresent, which means a field was
// introduced after a bunch of versions, we don't need to generate any code for
// the From impl.
(ItemStatus::NotPresent, ItemStatus::NotPresent) => None,
(
_,
Expand All @@ -186,95 +218,57 @@ impl VersionedField {
..
},
) => match direction {
Direction::Upgrade => match upgrade_with {
// The user specified a custom conversion function which
// will be used here instead of the default .into() call
// which utilizes From impls.
// FIXME (@Techassi): A custom conversion function needs
// to integrate with tracking as well.
Some(upgrade_fn) => Some(quote! {
#to_ident: #upgrade_fn(#from_struct_ident.#from_ident),
}),
// Default .into() call using From impls.
None => {
if self.nested {
let json_path_ident = to_ident.json_path_ident();

Some(quote! {
#to_ident: #from_struct_ident.#from_ident.tracking_into(status, &#json_path_ident),
})
} else {
Some(quote! {
#to_ident: #from_struct_ident.#from_ident.into(),
})
}
}
},
Direction::Downgrade => match downgrade_with {
Some(downgrade_fn) => Some(quote! {
#from_ident: #downgrade_fn(#from_struct_ident.#to_ident),
}),
None => {
if self.nested {
let json_path_ident = from_ident.json_path_ident();

Some(quote! {
#from_ident: #from_struct_ident.#to_ident.tracking_into(status, &#json_path_ident),
})
} else {
Some(quote! {
#from_ident: #from_struct_ident.#to_ident.into(),
})
}
}
},
Direction::Upgrade => Some(self.generate_from_impl_field(
to_ident,
from_struct_ident,
from_ident,
upgrade_with.as_ref(),
)),
Direction::Downgrade => Some(self.generate_from_impl_field(
from_ident,
from_struct_ident,
to_ident,
downgrade_with.as_ref(),
)),
},
(old, next) => {
let next_field_ident = next.get_ident();
let old_field_ident = old.get_ident();

// NOTE (@Techassi): Do we really need .into() here. I'm
// currently not sure why it is there and if it is needed
// in some edge cases.
// NOTE (@Techassi): Do we really need .into() here. I'm currently not sure
// why it is there and if it is needed in some edge cases.
match direction {
Direction::Upgrade => {
if self.nested {
let json_path_ident = next_field_ident.json_path_ident();

Some(quote! {
#next_field_ident: #from_struct_ident.#old_field_ident.tracking_into(status, &#json_path_ident),
})
} else {
Some(quote! {
#next_field_ident: #from_struct_ident.#old_field_ident.into(),
})
}
}
Direction::Downgrade => Some(quote! {
#old_field_ident: #from_struct_ident.#next_field_ident.into(),
}),
Direction::Upgrade => Some(self.generate_from_impl_field(
next_field_ident,
from_struct_ident,
old_field_ident,
None,
)),
Direction::Downgrade => Some(self.generate_from_impl_field(
old_field_ident,
from_struct_ident,
next_field_ident,
None,
)),
}
}
}
}
None => {
let field_ident = &self.idents.original;

if self.nested {
let json_path_ident = field_ident.json_path_ident();

Some(quote! {
#field_ident: #from_struct_ident.#field_ident.tracking_into(status, &#json_path_ident),
})
} else {
Some(quote! {
#field_ident: #from_struct_ident.#field_ident.into(),
})
}
Some(self.generate_from_impl_field(
field_ident,
from_struct_ident,
field_ident,
None,
))
}
}
}

/// Generates code needed when a tracked conversion for this field needs to be inserted into the
/// status.
pub fn generate_for_status_insertion(
&self,
direction: Direction,
Expand Down Expand Up @@ -317,6 +311,8 @@ impl VersionedField {
}
}

/// Generates code needed when a tracked conversion for this field needs to be removed from the
/// status.
pub fn generate_for_status_removal(
&self,
direction: Direction,
Expand Down Expand Up @@ -391,6 +387,66 @@ impl VersionedField {
}
}
}

/// Generates field definitions to be used inside `From` impl blocks.
fn generate_from_impl_field(
&self,
lhs_field_ident: &IdentString,
rhs_struct_ident: &IdentString,
rhs_field_ident: &IdentString,
custom_conversion_function: Option<&Path>,
) -> TokenStream {
match custom_conversion_function {
// The user specified a custom conversion function which will be used here instead of the
// default conversion call which utilizes From impls.
Some(convert_fn) => quote! {
#lhs_field_ident: #convert_fn(#rhs_struct_ident.#rhs_field_ident),
},
// Default conversion call using From impls.
None => {
if self.nested {
let json_path_ident = lhs_field_ident.json_path_ident();
let func = self.generate_tracking_conversion_function(json_path_ident);

quote! {
#lhs_field_ident: #rhs_struct_ident.#rhs_field_ident.#func,
}
} else {
let func = self.generate_conversion_function();

quote! {
#lhs_field_ident: #rhs_struct_ident.#rhs_field_ident.#func,
}
}
}
}
}

/// Generates tracking conversion functions used by field definitions in `From` impl blocks.
fn generate_tracking_conversion_function(&self, json_path_ident: IdentString) -> TokenStream {
match &self.hint {
Some(hint) => match hint {
Hint::Option => {
quote! { map(|v| v.tracking_into(status, &#json_path_ident)) }
}
Hint::Vec => {
quote! { into_iter().map(|v| v.tracking_into(status, &#json_path_ident)).collect() }
}
},
None => quote! { tracking_into(status, &#json_path_ident) },
}
}

/// Generates conversion functions used by field definitions in `From` impl blocks.
fn generate_conversion_function(&self) -> TokenStream {
match &self.hint {
Some(hint) => match hint {
Hint::Option => quote! { map(Into::into) },
Hint::Vec => quote! { into_iter().map(Into::into).collect() },
},
None => quote! { into() },
}
}
}

/// A collection of field idents used for different purposes.
Expand Down
Loading