Skip to content

feat(stackable-versioned): Generate downgrade From impls #1033

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
May 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c351c02
feat(stackable-versioned): Generate downgrade From impls
Techassi May 14, 2025
711aabe
feat(stackable-versioned): Generate primitive Status struct
Techassi May 14, 2025
92ed46c
feat(stackable-versioned): Emit status struct
Techassi May 15, 2025
857710f
chore: Fix clippy lints
Techassi May 16, 2025
14b09f2
fix(stackable-versioned): Emit correct code for modules
Techassi May 16, 2025
93570c6
test(stackable-versioned): Update snapshot tests
Techassi May 16, 2025
760a7bc
test(stackable-versioned): Update doc tests
Techassi May 16, 2025
3fc0b82
chore(stackable-versioned): Update changelog
Techassi May 16, 2025
bdf70e8
chore: Merge branch 'main' into feat/stackable-versioned-downgrade-fr…
Techassi May 16, 2025
1048bae
chore: Merge branch 'main' into feat/stackable-versioned-downgrade-fr…
Techassi May 21, 2025
6579eb0
chore(stackable-versioned): Remove unused dev dependencies
Techassi May 21, 2025
4f3a8d8
chore(stackable-versioned): Adjust {upgrade,downgrade}_with validation
Techassi May 21, 2025
c448ed2
chore(stackable-versioned): Improve K8s code generation
Techassi May 21, 2025
122e000
chore(stackable-versioned): Change crd_values to changed_values
Techassi May 21, 2025
0f7468b
test(stackable-versioned): Update snapshot tests
Techassi May 21, 2025
db47269
chore: Merge branch 'main' into feat/stackable-versioned-downgrade-fr…
Techassi May 21, 2025
634b977
chore(stackable-versioned): Clean up changelog
Techassi May 21, 2025
7fd4808
test(stackable-versioned): Update 'added' snapshot test
Techassi May 21, 2025
c613404
test(stackable-versioned): Fix and comment previously untested code
Techassi May 21, 2025
ee8a0a6
chore: Merge branch 'main' into feat/stackable-versioned-downgrade-fr…
Techassi May 21, 2025
8b095da
docs(stackable-versioned): Update 'convert_with' doc comment
Techassi May 22, 2025
229dcef
test(stackable-versioned): Rename convert_with test to downgrade_with
Techassi May 22, 2025
fd4d1ad
chore(stackable-versioned): Add disclaimer comment
Techassi May 22, 2025
436e294
chore: Merge branch 'main' into feat/stackable-versioned-downgrade-fr…
Techassi May 22, 2025
abf5deb
refactor(stackable-versioned): Make conversion tracking opt in
Techassi May 22, 2025
1a0fcc3
refactor(stackable-versioned): Move status struct behind feature flag
Techassi May 22, 2025
44bb861
test(stackable-versioned): Add passing Kubernetes compile tests
Techassi May 22, 2025
69ee88c
fix(stackable-versioned): Use correct module name
Techassi May 22, 2025
e38161b
chore(stackable-versioned): Update status struct name to avoid collis…
Techassi May 22, 2025
1fdc2aa
chore(stackable-versioned): Update changelog
Techassi May 22, 2025
46da1d6
test(stackable-versioned): Update status struct name
Techassi May 22, 2025
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
5 changes: 5 additions & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion crates/k8s-version/src/api_version/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ impl<'de> Deserialize<'de> for ApiVersion {
{
struct ApiVersionVisitor;

impl<'de> Visitor<'de> for ApiVersionVisitor {
impl Visitor<'_> for ApiVersionVisitor {
type Value = ApiVersion;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
Expand Down
2 changes: 1 addition & 1 deletion crates/k8s-version/src/level/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ impl<'de> Deserialize<'de> for Level {
{
struct LevelVisitor;

impl<'de> Visitor<'de> for LevelVisitor {
impl Visitor<'_> for LevelVisitor {
type Value = Level;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
Expand Down
2 changes: 1 addition & 1 deletion crates/k8s-version/src/version/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ impl<'de> Deserialize<'de> for Version {
{
struct VersionVisitor;

impl<'de> Visitor<'de> for VersionVisitor {
impl Visitor<'_> for VersionVisitor {
type Value = Version;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
Expand Down
1 change: 0 additions & 1 deletion crates/stackable-versioned-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ quote.workspace = true
[dev-dependencies]
# Only needed for doc tests / examples
stackable-versioned = { path = "../stackable-versioned", features = ["k8s"] }
k8s-openapi.workspace = true

insta.workspace = true
prettyplease.workspace = true
Expand Down
35 changes: 25 additions & 10 deletions crates/stackable-versioned-macros/src/attrs/item/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,15 +238,26 @@ impl CommonItemAttributes {
}
}

// The convert_with argument only makes sense to use when the
// type changed
if let Some(convert_func) = change.convert_with.as_ref() {
if change.from_type.is_none() {
if change.from_type.is_none() {
// The upgrade_with argument only makes sense to use when the
// type changed
if let Some(upgrade_func) = change.upgrade_with.as_ref() {
errors.push(
Error::custom(
"the `convert_with` argument must be used in combination with `from_type`",
"the `upgrade_with` argument must be used in combination with `from_type`",
)
.with_span(&convert_func.span()),
.with_span(&upgrade_func.span()),
);
}

// The downgrade_with argument only makes sense to use when the
// type changed
if let Some(downgrade_func) = change.downgrade_with.as_ref() {
errors.push(
Error::custom(
"the `downgrade_with` argument must be used in combination with `from_type`",
)
.with_span(&downgrade_func.span()),
);
}
}
Expand Down Expand Up @@ -315,7 +326,8 @@ impl CommonItemAttributes {
.unwrap_or(ty.clone());

actions.insert(*change.since, ItemStatus::Change {
convert_with: change.convert_with.as_deref().cloned(),
downgrade_with: change.downgrade_with.as_deref().cloned(),
upgrade_with: change.upgrade_with.as_deref().cloned(),
from_ident: from_ident.clone(),
from_type: from_ty.clone(),
to_ident: ident,
Expand Down Expand Up @@ -359,7 +371,8 @@ impl CommonItemAttributes {
.unwrap_or(ty.clone());

actions.insert(*change.since, ItemStatus::Change {
convert_with: change.convert_with.as_deref().cloned(),
downgrade_with: change.downgrade_with.as_deref().cloned(),
upgrade_with: change.upgrade_with.as_deref().cloned(),
from_ident: from_ident.clone(),
from_type: from_ty.clone(),
to_ident: ident,
Expand Down Expand Up @@ -429,13 +442,15 @@ fn default_default_fn() -> SpannedValue<Path> {
/// Example usage:
/// - `changed(since = "...", from_name = "...")`
/// - `changed(since = "...", from_name = "...", from_type="...")`
/// - `changed(since = "...", from_name = "...", from_type="...", convert_with = "...")`
/// - `changed(since = "...", from_name = "...", from_type="...", upgrade_with = "...")`
/// - `changed(since = "...", from_name = "...", from_type="...", downgrade_with = "...")`
#[derive(Clone, Debug, FromMeta)]
pub struct ChangedAttributes {
pub since: SpannedValue<Version>,
pub from_name: Option<SpannedValue<String>>,
pub from_type: Option<SpannedValue<Type>>,
pub convert_with: Option<SpannedValue<Path>>,
pub upgrade_with: Option<SpannedValue<Path>>,
pub downgrade_with: Option<SpannedValue<Path>>,
}

/// For the deprecated() action
Expand Down
47 changes: 28 additions & 19 deletions crates/stackable-versioned-macros/src/attrs/k8s.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,30 @@ use syn::Path;
/// times.
/// - `skip`: Controls skipping parts of the generation.
#[derive(Clone, Debug, FromMeta)]
pub(crate) struct KubernetesArguments {
pub(crate) group: String,
pub(crate) kind: Option<String>,
pub(crate) singular: Option<String>,
pub(crate) plural: Option<String>,
pub(crate) namespaced: Flag,
pub struct KubernetesArguments {
pub group: String,
pub kind: Option<String>,
pub singular: Option<String>,
pub plural: Option<String>,
pub namespaced: Flag,
// root
pub(crate) crates: Option<KubernetesCrateArguments>,
pub(crate) status: Option<Path>,
pub crates: Option<KubernetesCrateArguments>,
pub status: Option<Path>,
// derive
// schema
// scale
// printcolumn
#[darling(multiple, rename = "shortname")]
pub(crate) shortnames: Vec<String>,
pub shortnames: Vec<String>,
// category
// selectable
// doc
// annotation
// label
pub(crate) skip: Option<KubernetesSkipArguments>,
pub skip: Option<KubernetesSkipArguments>,

#[darling(default)]
pub options: RawKubernetesOptions,
}

/// This struct contains supported kubernetes skip arguments.
Expand All @@ -52,19 +55,25 @@ pub(crate) struct KubernetesArguments {
/// - `merged_crd` flag, which skips generating the `crd()` and `merged_crd()` functions are
/// generated.
#[derive(Clone, Debug, FromMeta)]
pub(crate) struct KubernetesSkipArguments {
pub struct KubernetesSkipArguments {
/// Whether the `crd()` and `merged_crd()` generation should be skipped for
/// this container.
pub(crate) merged_crd: Flag,
pub merged_crd: Flag,
}

/// This struct contains crate overrides to be passed to `#[kube]`.
#[derive(Clone, Debug, FromMeta)]
pub(crate) struct KubernetesCrateArguments {
pub(crate) kube_core: Option<Path>,
pub(crate) kube_client: Option<Path>,
pub(crate) k8s_openapi: Option<Path>,
pub(crate) schemars: Option<Path>,
pub(crate) serde: Option<Path>,
pub(crate) serde_json: Option<Path>,
pub struct KubernetesCrateArguments {
pub kube_core: Option<Path>,
pub kube_client: Option<Path>,
pub k8s_openapi: Option<Path>,
pub schemars: Option<Path>,
pub serde: Option<Path>,
pub serde_json: Option<Path>,
pub versioned: Option<Path>,
}

#[derive(Clone, Default, Debug, FromMeta)]
pub struct RawKubernetesOptions {
pub experimental_conversion_tracking: Flag,
}
95 changes: 73 additions & 22 deletions crates/stackable-versioned-macros/src/codegen/container/enum.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::ops::Not;

use darling::{FromAttributes, Result, util::IdentString};
use darling::{FromAttributes, Result};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Generics, ItemEnum};
Expand Down Expand Up @@ -126,11 +126,11 @@ impl Enum {
}

/// Generates code for the `From<Version> for NextVersion` implementation.
pub(crate) fn generate_from_impl(
pub fn generate_upgrade_from_impl(
&self,
version: &VersionDefinition,
next_version: Option<&VersionDefinition>,
is_nested: bool,
add_attributes: bool,
) -> Option<TokenStream> {
if version.skip_from || self.common.options.skip_from {
return None;
Expand All @@ -145,12 +145,18 @@ impl Enum {
// later versions.
let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl();
let enum_ident = &self.common.idents.original;
let from_ident = &self.common.idents.from;
let from_enum_ident = &self.common.idents.from;

let next_version_ident = &next_version.ident;
let version_ident = &version.ident;
let for_module_ident = &next_version.ident;
let from_module_ident = &version.ident;

let variants = self.generate_from_variants(version, next_version, enum_ident);
let variants: TokenStream = self
.variants
.iter()
.filter_map(|v| {
v.generate_for_upgrade_from_impl(version, next_version, enum_ident)
})
.collect();

// Include allow(deprecated) only when this or the next version is
// deprecated. Also include it, when a variant in this or the next
Expand All @@ -163,17 +169,18 @@ impl Enum {

// Only add the #[automatically_derived] attribute only if this impl is used
// outside of a module (in standalone mode).
let automatically_derived =
is_nested.not().then(|| quote! {#[automatically_derived]});
let automatically_derived = add_attributes
.not()
.then(|| quote! {#[automatically_derived]});

Some(quote! {
#automatically_derived
#allow_attribute
impl #impl_generics ::std::convert::From<#version_ident::#enum_ident #type_generics> for #next_version_ident::#enum_ident #type_generics
impl #impl_generics ::std::convert::From<#from_module_ident::#enum_ident #type_generics> for #for_module_ident::#enum_ident #type_generics
#where_clause
{
fn from(#from_ident: #version_ident::#enum_ident #type_generics) -> Self {
match #from_ident {
fn from(#from_enum_ident: #from_module_ident::#enum_ident #type_generics) -> Self {
match #from_enum_ident {
#variants
}
}
Expand All @@ -184,20 +191,64 @@ impl Enum {
}
}

/// Generates code for enum variants used in `From` implementations.
fn generate_from_variants(
pub fn generate_downgrade_from_impl(
&self,
version: &VersionDefinition,
next_version: &VersionDefinition,
enum_ident: &IdentString,
) -> TokenStream {
let mut tokens = TokenStream::new();

for variant in &self.variants {
tokens.extend(variant.generate_for_from_impl(version, next_version, enum_ident));
next_version: Option<&VersionDefinition>,
add_attributes: bool,
) -> Option<TokenStream> {
if version.skip_from || self.common.options.skip_from {
return None;
}

tokens
match next_version {
Some(next_version) => {
let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl();
let enum_ident = &self.common.idents.original;
let from_enum_ident = &self.common.idents.from;

let for_module_ident = &version.ident;
let from_module_ident = &next_version.ident;

let variants: TokenStream = self
.variants
.iter()
.filter_map(|v| {
v.generate_for_downgrade_from_impl(version, next_version, enum_ident)
})
.collect();

// Include allow(deprecated) only when this or the next version is
// deprecated. Also include it, when a variant in this or the next
// version is deprecated.
let allow_attribute = (version.deprecated.is_some()
|| next_version.deprecated.is_some()
|| self.is_any_variant_deprecated(version)
|| self.is_any_variant_deprecated(next_version))
.then_some(quote! { #[allow(deprecated)] });

// Only add the #[automatically_derived] attribute only if this impl is used
// outside of a module (in standalone mode).
let automatically_derived = add_attributes
.not()
.then(|| quote! {#[automatically_derived]});

Some(quote! {
#automatically_derived
#allow_attribute
impl #impl_generics ::std::convert::From<#from_module_ident::#enum_ident #type_generics> for #for_module_ident::#enum_ident #type_generics
#where_clause
{
fn from(#from_enum_ident: #from_module_ident::#enum_ident #type_generics) -> Self {
match #from_enum_ident {
#variants
}
}
}
})
}
None => None,
}
}

/// Returns whether any variant is deprecated in the provided `version`.
Expand Down
Loading
Loading