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.

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
132 changes: 117 additions & 15 deletions crates/stackable-versioned-macros/src/codegen/item/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ impl VersionedField {
Direction::Downgrade => {
let next_change = changes.get_expect(&next_version.inner);

let serde_yaml_path = &*mod_gen_ctx.crates.serde_yaml;
let serde_json_path = &*mod_gen_ctx.crates.serde_json;
let versioned_path = &*mod_gen_ctx.crates.versioned;

match next_change {
Expand All @@ -318,7 +318,90 @@ impl VersionedField {
Some(quote! {
upgrades.push(#versioned_path::ChangedValue {
json_path: #json_path_ident,
value: #serde_yaml_path::to_value(&#from_struct_ident.#ident).unwrap(),
value: #serde_json_path::to_value(&#from_struct_ident.#ident).unwrap(),
downgraded_value: ::core::option::Option::None,
});
})
}
_ => None,
}
}
}
}

/// Generates code which serializes the value of this field before it is converted. This is only
/// needed for fields which changed their type, because the conversion consumes the value.
///
/// - When downgrading, this is the value of the newer type, which is tracked in the status.
/// - When upgrading, this is the value of the older type, which is compared against the tracked
/// downgraded value to detect if a user changed the field in the older version.
pub fn generate_for_tracked_value(
&self,
direction: Direction,
next_version: &VersionDefinition,
from_struct_ident: &IdentString,
mod_gen_ctx: ModuleGenerationContext<'_>,
) -> Option<TokenStream> {
let changes = self.changes.as_ref()?;
let next_change = changes.get_expect(&next_version.inner);

match next_change {
ItemStatus::Change {
from_ident,
to_ident,
..
} if next_change.is_type_change() => {
let serde_json_path = &*mod_gen_ctx.crates.serde_json;
let value_ident = to_ident.tracked_value_ident();

let field_ident = match direction {
Direction::Upgrade => from_ident,
Direction::Downgrade => to_ident,
};

Some(quote! {
let #value_ident = #serde_json_path::to_value(&#from_struct_ident.#field_ident).unwrap();
})
}
_ => None,
}
}

/// Generates code needed when a tracked type change of this field needs to be inserted into the
/// status. In contrast to added fields, this can only be done after the conversion, because the
/// downgraded value is tracked as well.
pub fn generate_for_status_post_insertion(
&self,
direction: Direction,
next_version: &VersionDefinition,
mod_gen_ctx: ModuleGenerationContext<'_>,
) -> Option<TokenStream> {
let changes = self.changes.as_ref()?;

match direction {
Direction::Upgrade => None,
Direction::Downgrade => {
let next_change = changes.get_expect(&next_version.inner);

let serde_json_path = &*mod_gen_ctx.crates.serde_json;
let versioned_path = &*mod_gen_ctx.crates.versioned;

match next_change {
ItemStatus::Change {
from_ident,
to_ident,
..
} if next_change.is_type_change() => {
let json_path_ident = to_ident.json_path_ident();
let value_ident = to_ident.tracked_value_ident();

Some(quote! {
upgrades.push(#versioned_path::ChangedValue {
json_path: #json_path_ident,
value: #value_ident,
downgraded_value: ::core::option::Option::Some(
#serde_json_path::to_value(&spec.#from_ident).unwrap()
),
});
})
}
Expand All @@ -334,6 +417,7 @@ impl VersionedField {
&self,
direction: Direction,
next_version: &VersionDefinition,
mod_gen_ctx: ModuleGenerationContext<'_>,
) -> Option<TokenStream> {
// If there are no changes for this field, there is also no need to generate a match arm
// for applying a tracked value.
Expand All @@ -342,16 +426,33 @@ impl VersionedField {
match direction {
Direction::Upgrade => {
let next_change = changes.get_expect(&next_version.inner);
let serde_json_path = &*mod_gen_ctx.crates.serde_json;

match next_change {
// NOTE (@Techassi): We currently only support tracking added fields. As such
// we only need to generate code if the next change is "Addition".
ItemStatus::Addition { ident, .. } => {
let json_path_ident = ident.json_path_ident();

Some(quote! {
json_path if json_path == #json_path_ident => {
spec.#ident = serde_yaml::from_value(value).unwrap();
spec.#ident = #serde_json_path::from_value(value).unwrap();
},
})
}
// The tracked value is only applied if the field still contains the value it
// was downgraded to. Otherwise, a user changed the field in the older version
// and that change takes precedence over the tracked value.
//
// A downgraded value of null is serialized as `downgradedValue: null`, which is
// deserialized as None. As such, a missing downgraded value is treated as null.
ItemStatus::Change { to_ident, .. } if next_change.is_type_change() => {
let json_path_ident = to_ident.json_path_ident();
let value_ident = to_ident.tracked_value_ident();

Some(quote! {
json_path if json_path == #json_path_ident => {
if downgraded_value.unwrap_or_default() == #value_ident {
spec.#to_ident = #serde_json_path::from_value(value).unwrap();
}
},
})
}
Expand Down Expand Up @@ -390,17 +491,18 @@ impl VersionedField {
(Some(changes), _) => {
let next_change = changes.get_expect(&next_version.inner);

match next_change {
ItemStatus::Addition { ident, .. } => {
let field_ident = ident.json_path_ident();
let child_string = ident.to_string();
let ident = match next_change {
ItemStatus::Addition { ident, .. } => ident,
ItemStatus::Change { to_ident, .. } if next_change.is_type_change() => to_ident,
_ => return None,
};

Some(quote! {
let #field_ident = #versioned_path::jthong_path(parent, #child_string);
})
}
_ => None,
}
let field_ident = ident.json_path_ident();
let child_string = ident.to_string();

Some(quote! {
let #field_ident = #versioned_path::jthong_path(parent, #child_string);
})
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions crates/stackable-versioned-macros/src/codegen/item/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,11 @@ impl ItemStatus {
Self::NotPresent => unreachable!("ItemStatus::NotPresent does not have an ident"),
}
}

/// Returns `true` if this status is a change which modified the type of the item.
///
/// Changes which only rename the item are lossless and as such don't need to be tracked.
pub fn is_type_change(&self) -> bool {
matches!(self, Self::Change { from_type, to_type, .. } if from_type != to_type)
}
}
Loading
Loading