From c031faab95b79907b89ba577d24a8d19ba24c9c6 Mon Sep 17 00:00:00 2001 From: Techassi Date: Mon, 14 Oct 2024 16:09:36 +0200 Subject: [PATCH 01/33] chore: Add changelog entry --- crates/stackable-versioned/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/stackable-versioned/CHANGELOG.md b/crates/stackable-versioned/CHANGELOG.md index e36236028..6499f717d 100644 --- a/crates/stackable-versioned/CHANGELOG.md +++ b/crates/stackable-versioned/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Add support to apply the `#[versioned()]` macro to modules to version all contained items at + once ([#abc]). + +[#abc]: https://github.com/stackabletech/operator-rs/pull/abc + ## [0.4.0] - 2024-10-14 ### Added From cd1fb246eea3974485b76729ef49d39e20bfd0aa Mon Sep 17 00:00:00 2001 From: Techassi Date: Mon, 14 Oct 2024 16:11:55 +0200 Subject: [PATCH 02/33] chore: Update PR link in changelog --- crates/stackable-versioned/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/stackable-versioned/CHANGELOG.md b/crates/stackable-versioned/CHANGELOG.md index 6499f717d..ca1698c19 100644 --- a/crates/stackable-versioned/CHANGELOG.md +++ b/crates/stackable-versioned/CHANGELOG.md @@ -7,9 +7,9 @@ All notable changes to this project will be documented in this file. ### Added - Add support to apply the `#[versioned()]` macro to modules to version all contained items at - once ([#abc]). + once ([#891]). -[#abc]: https://github.com/stackabletech/operator-rs/pull/abc +[#891]: https://github.com/stackabletech/operator-rs/pull/891 ## [0.4.0] - 2024-10-14 From 1dd3047925b92c523ff95ca36fc060fadee2988e Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 18 Oct 2024 09:11:11 +0200 Subject: [PATCH 03/33] refactor: Modularize token generation This separates token generation code to be more modular. Depending on the context, individual parts of the code need to be placed in different locations. Two major refactors are the change of macro input as well as moving module generation out of the struct definition generation. This commit doesn't include changes for enum generation. This will be done in a separate commit. --- ...versioned_macros__test__k8s_snapshots.snap | 2 +- .../src/attrs/common/container.rs | 122 +++++------ .../src/attrs/common/item.rs | 7 +- .../src/attrs/common/k8s.rs | 32 +++ .../src/attrs/common/mod.rs | 35 +++ .../src/attrs/common/module.rs | 30 +++ .../src/codegen/common/container.rs | 74 +++++-- .../src/codegen/common/item.rs | 16 +- .../src/codegen/common/mod.rs | 29 ++- .../src/codegen/common/module.rs | 31 +++ .../src/codegen/mod.rs | 56 +---- .../src/codegen/venum/mod.rs | 34 +-- .../src/codegen/venum/variant.rs | 18 +- .../src/codegen/vmod/mod.rs | 51 +++++ .../src/codegen/vstruct/field.rs | 18 +- .../src/codegen/vstruct/mod.rs | 207 +++++++++++------- crates/stackable-versioned-macros/src/lib.rs | 92 +++++++- .../src/test_utils.rs | 4 +- 18 files changed, 568 insertions(+), 290 deletions(-) create mode 100644 crates/stackable-versioned-macros/src/attrs/common/k8s.rs create mode 100644 crates/stackable-versioned-macros/src/attrs/common/module.rs create mode 100644 crates/stackable-versioned-macros/src/codegen/common/module.rs create mode 100644 crates/stackable-versioned-macros/src/codegen/vmod/mod.rs diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots.snap index df79bb1da..6aeb1cc82 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots.snap @@ -106,7 +106,7 @@ impl Foo { vec![ < v1alpha1::Foo as ::kube::CustomResourceExt > ::crd(), < v1beta1::Foo as ::kube::CustomResourceExt > ::crd(), < v1::Foo as - ::kube::CustomResourceExt > ::crd() + ::kube::CustomResourceExt > ::crd(), ], &stored_apiversion.to_string(), ) diff --git a/crates/stackable-versioned-macros/src/attrs/common/container.rs b/crates/stackable-versioned-macros/src/attrs/common/container.rs index 434c9e1ab..42f942e27 100644 --- a/crates/stackable-versioned-macros/src/attrs/common/container.rs +++ b/crates/stackable-versioned-macros/src/attrs/common/container.rs @@ -1,33 +1,57 @@ +//! This module contains attributes which can be used on containers (structs and enums). +//! +//! Generally there are two different containers, called "standalone" and "nested" based on which +//! context they are used in. Standalone containers define versioning directly on the struct or +//! enum. This is useful for single versioned containers. This type of versioning is fine as long +//! as there is no other versioned container in the same file using the same versions. If that is +//! the case, the generated modules will collide. There are two possible solutions for this: move +//! each versioned container into its own file or use the nested declarations. It should be noted +//! that there might be cases where it is fine to separate each container into its own file. One +//! such case is when each container serves distinctively different use-cases and provide numerous +//! associated items like functions. +//! +//! In cases where separate files are not desired, the nested mode can be used. The nested mode +//! allows to declare versions on a module which contains containers which shall be versioned +//! according to the defined versions. This approach allows defining multiple versioned containers +//! in the same file without module collisions. +//! +//! The attributes used must be tailored to both of these two modes, because not all arguments are +//! valid in all modes. As such different attributes allow different validation mechanisms. One +//! such an example is that nested containers must not define versions as the definition is done +//! on the module. This is in direct contrast to containers used in standalone mode. + use std::{cmp::Ordering, ops::Deref}; use darling::{ util::{Flag, SpannedValue}, - Error, FromMeta, Result, + Error, FromAttributes, FromMeta, Result, }; use itertools::Itertools; -use k8s_version::Version; -/// This struct contains supported container attributes. +use crate::attrs::common::{KubernetesArguments, SkipArguments, VersionArguments}; + +/// This struct contains supported container attributes which can be applied to structs and enums. /// /// Currently supported attributes are: /// /// - `version`, which can occur one or more times. See [`VersionAttributes`]. +/// - `k8s`, which enables Kubernetes specific features and allows customization if these features. /// - `options`, which allow further customization of the generated code. -/// See [`ContainerAttributes`]. +/// See [`StandaloneOptionArguments`]. #[derive(Debug, FromMeta)] -#[darling(and_then = ContainerAttributes::validate)] -pub(crate) struct ContainerAttributes { +#[darling(and_then = StandaloneContainerAttributes::validate)] +pub(crate) struct StandaloneContainerAttributes { #[darling(multiple, rename = "version")] - pub(crate) versions: SpannedValue>, + pub(crate) versions: SpannedValue>, #[darling(rename = "k8s")] - pub(crate) kubernetes_attrs: Option, + pub(crate) kubernetes_args: Option, #[darling(default, rename = "options")] - pub(crate) common_option_attrs: OptionAttributes, + pub(crate) common_option_args: StandaloneOptionArguments, } -impl ContainerAttributes { +impl StandaloneContainerAttributes { fn validate(mut self) -> Result { // Most of the validation for individual version strings is done by the // k8s-version crate. That's why the code below only checks that at @@ -47,7 +71,7 @@ impl ContainerAttributes { // Ensure that versions are defined in sorted (ascending) order to keep // code consistent. - if !self.common_option_attrs.allow_unsorted.is_present() { + if !self.common_option_args.allow_unsorted.is_present() { let original = self.versions.deref().clone(); self.versions .sort_by(|lhs, rhs| lhs.name.partial_cmp(&rhs.name).unwrap_or(Ordering::Equal)); @@ -91,7 +115,7 @@ impl ContainerAttributes { // Ensure that the 'k8s' feature is enabled when the 'k8s()' // attribute is used. - if self.kubernetes_attrs.is_some() && cfg!(not(feature = "k8s")) { + if self.kubernetes_args.is_some() && cfg!(not(feature = "k8s")) { return Err(Error::custom( "the `#[versioned(k8s())]` attribute can only be used when the `k8s` feature is enabled", )); @@ -101,74 +125,30 @@ impl ContainerAttributes { } } -/// This struct contains supported version options. -/// -/// Supported options are: -/// -/// - `name` of the version, like `v1alpha1`. -/// - `deprecated` flag to mark that version as deprecated. -/// - `skip` option to skip generating various pieces of code. -/// - `doc` option to add version-specific documentation. -#[derive(Clone, Debug, FromMeta)] -pub(crate) struct VersionAttributes { - pub(crate) deprecated: Flag, - pub(crate) name: Version, - pub(crate) skip: Option, - pub(crate) doc: Option, -} - -/// This struct contains supported option attributes. +/// This struct contains supported option arguments for containers used in standalone mode. /// -/// Supported attributes are: +/// Supported arguments are: /// -/// - `allow_unsorted`, which allows declaring versions in unsorted order, -/// instead of enforcing ascending order. +/// - `allow_unsorted`, which allows declaring versions in unsorted order, instead of enforcing +/// ascending order. /// - `skip` option to skip generating various pieces of code. #[derive(Clone, Debug, Default, FromMeta)] -pub(crate) struct OptionAttributes { +pub(crate) struct StandaloneOptionArguments { pub(crate) allow_unsorted: Flag, - pub(crate) skip: Option, + pub(crate) skip: Option, } -/// This struct contains supported Kubernetes attributes. -/// -/// Supported attributes are: -/// -/// - `skip`, which controls skipping parts of the generation. -/// - `kind`, which allows overwriting the kind field of the CRD. This defaults -/// to the struct name (without the 'Spec' suffix). -/// - `group`, which sets the CRD group, usually the domain of the company. -#[derive(Clone, Debug, FromMeta)] -pub(crate) struct KubernetesAttributes { - pub(crate) skip: Option, - pub(crate) singular: Option, - pub(crate) plural: Option, - pub(crate) kind: Option, - pub(crate) namespaced: Flag, - pub(crate) group: String, -} +#[derive(Debug, FromAttributes)] +#[darling(attributes(versioned))] +pub(crate) struct NestedContainerAttributes { + #[darling(rename = "k8s")] + pub(crate) kubernetes_args: Option, -/// This struct contains supported kubernetes skip attributes. -/// -/// Supported attributes are: -/// -/// - `merged_crd` flag, which skips generating the `crd()` and `merged_crd()` -/// functions are generated. -#[derive(Clone, Debug, FromMeta)] -pub(crate) struct KubernetesSkipAttributes { - /// Whether the `crd()` and `merged_crd()` generation should be skipped for - /// this container. - pub(crate) merged_crd: Flag, + #[darling(default, rename = "options")] + pub(crate) common_option_args: NestedOptionArguments, } -/// This struct contains supported common skip attributes. -/// -/// Supported attributes are: -/// -/// - `from` flag, which skips generating [`From`] implementations when provided. #[derive(Clone, Debug, Default, FromMeta)] -pub(crate) struct CommonSkipAttributes { - /// Whether the [`From`] implementation generation should be skipped for all - /// versions of this container. - pub(crate) from: Flag, +pub(crate) struct NestedOptionArguments { + pub(crate) skip: Option, } diff --git a/crates/stackable-versioned-macros/src/attrs/common/item.rs b/crates/stackable-versioned-macros/src/attrs/common/item.rs index 42b6ac953..5c7c51b21 100644 --- a/crates/stackable-versioned-macros/src/attrs/common/item.rs +++ b/crates/stackable-versioned-macros/src/attrs/common/item.rs @@ -4,7 +4,7 @@ use proc_macro2::Span; use syn::{spanned::Spanned, Attribute, Ident, Path, Type}; use crate::{ - attrs::common::ContainerAttributes, + attrs::common::StandaloneContainerAttributes, codegen::common::Attributes, consts::{DEPRECATED_FIELD_PREFIX, DEPRECATED_VARIANT_PREFIX}, }; @@ -23,7 +23,7 @@ where /// declared container versions. fn validate_versions( &self, - container_attrs: &ContainerAttributes, + container_attrs: &StandaloneContainerAttributes, item: &I, ) -> Result<(), darling::Error>; } @@ -35,7 +35,7 @@ where { fn validate_versions( &self, - container_attrs: &ContainerAttributes, + container_attrs: &StandaloneContainerAttributes, item: &I, ) -> Result<(), darling::Error> { // NOTE (@Techassi): Can we maybe optimize this a little? @@ -360,7 +360,6 @@ fn default_default_fn() -> SpannedValue { pub(crate) struct ChangedAttributes { pub(crate) since: SpannedValue, pub(crate) from_name: Option>, - pub(crate) from_type: Option>, } diff --git a/crates/stackable-versioned-macros/src/attrs/common/k8s.rs b/crates/stackable-versioned-macros/src/attrs/common/k8s.rs new file mode 100644 index 000000000..88e1ed051 --- /dev/null +++ b/crates/stackable-versioned-macros/src/attrs/common/k8s.rs @@ -0,0 +1,32 @@ +use darling::{util::Flag, FromMeta}; + +/// This struct contains supported Kubernetes arguments. +/// +/// Supported arguments are: +/// +/// - `skip`, which controls skipping parts of the generation. +/// - `kind`, which allows overwriting the kind field of the CRD. This defaults to the struct name +/// (without the 'Spec' suffix). +/// - `group`, which sets the CRD group, usually the domain of the company. +#[derive(Clone, Debug, FromMeta)] +pub(crate) struct KubernetesArguments { + pub(crate) skip: Option, + pub(crate) singular: Option, + pub(crate) plural: Option, + pub(crate) kind: Option, + pub(crate) namespaced: Flag, + pub(crate) group: String, +} + +/// This struct contains supported kubernetes skip arguments. +/// +/// Supported arguments are: +/// +/// - `merged_crd` flag, which skips generating the `crd()` and `merged_crd()` functions are +/// generated. +#[derive(Clone, Debug, FromMeta)] +pub(crate) struct KubernetesSkipArguments { + /// Whether the `crd()` and `merged_crd()` generation should be skipped for + /// this container. + pub(crate) merged_crd: Flag, +} diff --git a/crates/stackable-versioned-macros/src/attrs/common/mod.rs b/crates/stackable-versioned-macros/src/attrs/common/mod.rs index 175e8eed5..7c232273f 100644 --- a/crates/stackable-versioned-macros/src/attrs/common/mod.rs +++ b/crates/stackable-versioned-macros/src/attrs/common/mod.rs @@ -1,5 +1,40 @@ +use darling::{util::Flag, FromMeta}; +use k8s_version::Version; + mod container; mod item; +mod k8s; +mod module; pub(crate) use container::*; pub(crate) use item::*; +pub(crate) use k8s::*; +pub(crate) use module::*; + +/// This struct contains supported version arguments. +/// +/// Supported arguments are: +/// +/// - `name` of the version, like `v1alpha1`. +/// - `deprecated` flag to mark that version as deprecated. +/// - `skip` option to skip generating various pieces of code. +/// - `doc` option to add version-specific documentation. +#[derive(Clone, Debug, FromMeta)] +pub(crate) struct VersionArguments { + pub(crate) deprecated: Flag, + pub(crate) name: Version, + pub(crate) skip: Option, + pub(crate) doc: Option, +} + +/// This struct contains supported common skip arguments. +/// +/// Supported arguments are: +/// +/// - `from` flag, which skips generating [`From`] implementations when provided. +#[derive(Clone, Debug, Default, FromMeta)] +pub(crate) struct SkipArguments { + /// Whether the [`From`] implementation generation should be skipped for all versions of this + /// container. + pub(crate) from: Flag, +} diff --git a/crates/stackable-versioned-macros/src/attrs/common/module.rs b/crates/stackable-versioned-macros/src/attrs/common/module.rs new file mode 100644 index 000000000..5d0f48268 --- /dev/null +++ b/crates/stackable-versioned-macros/src/attrs/common/module.rs @@ -0,0 +1,30 @@ +use darling::{ + util::{Flag, SpannedValue}, + FromMeta, Result, +}; + +use crate::attrs::common::{SkipArguments, VersionArguments}; + +#[derive(Debug, FromMeta)] +#[darling(and_then = ModuleAttributes::validate)] +pub(crate) struct ModuleAttributes { + #[darling(multiple, rename = "version")] + pub(crate) versions: SpannedValue>, + + #[darling(default, rename = "options")] + pub(crate) common_option_args: ModuleOptionArguments, +} + +impl ModuleAttributes { + fn validate(self) -> Result { + // TODO (@Techassi): Make this actually validate + Ok(self) + } +} + +#[derive(Debug, Default, FromMeta)] +pub(crate) struct ModuleOptionArguments { + pub(crate) skip: Option, + pub(crate) preserve_module: Flag, + pub(crate) allow_unsorted: Flag, +} diff --git a/crates/stackable-versioned-macros/src/codegen/common/container.rs b/crates/stackable-versioned-macros/src/codegen/common/container.rs index 4472552d9..40b8d0a0f 100644 --- a/crates/stackable-versioned-macros/src/codegen/common/container.rs +++ b/crates/stackable-versioned-macros/src/codegen/common/container.rs @@ -1,12 +1,17 @@ use std::ops::Deref; use convert_case::{Case, Casing}; +use darling::util::IdentString; use k8s_version::Version; use proc_macro2::TokenStream; use quote::format_ident; use syn::{Attribute, Ident, Visibility}; -use crate::{attrs::common::ContainerAttributes, codegen::common::ContainerVersion}; +use crate::{ + attrs::common::StandaloneContainerAttributes, + codegen::common::VersionDefinition, + consts::{DEPRECATED_FIELD_PREFIX, DEPRECATED_VARIANT_PREFIX}, +}; /// This trait helps to unify versioned containers, like structs and enums. /// @@ -24,7 +29,11 @@ where Self: Sized + Deref>, { /// Creates a new versioned container. - fn new(input: ContainerInput, data: D, attributes: ContainerAttributes) -> syn::Result; + fn new( + input: ContainerInput, + data: D, + attributes: StandaloneContainerAttributes, + ) -> syn::Result; /// This generates the complete code for a single versioned container. /// @@ -32,25 +41,48 @@ where /// contains the container with the appropriate items (fields or variants) /// Additionally, it generates `From` implementations, which enable /// conversion from an older to a newer version. - fn generate_tokens(&self) -> TokenStream; + fn generate_standalone_tokens(&self) -> TokenStream; + + fn generate_nested_tokens(&self) -> TokenStream; } -/// Provides extra functionality on top of [`struct@Ident`]s. -pub(crate) trait IdentExt { +/// Provides extra functionality on top of [`struct@Ident`]s used to name containers. +pub(crate) trait ContainerIdentExt { /// Removes the 'Spec' suffix from the [`struct@Ident`]. - fn as_cleaned_kubernetes_ident(&self) -> Ident; + fn as_cleaned_kubernetes_ident(&self) -> IdentString; /// Transforms the [`struct@Ident`] into one usable in the [`From`] impl. - fn as_from_impl_ident(&self) -> Ident; + fn as_from_impl_ident(&self) -> IdentString; } -impl IdentExt for Ident { - fn as_cleaned_kubernetes_ident(&self) -> Ident { - format_ident!("{}", self.to_string().trim_end_matches("Spec")) +impl ContainerIdentExt for Ident { + fn as_cleaned_kubernetes_ident(&self) -> IdentString { + let ident = format_ident!("{}", self.to_string().trim_end_matches("Spec")); + IdentString::new(ident) + } + + fn as_from_impl_ident(&self) -> IdentString { + let ident = format_ident!("__sv_{}", self.to_string().to_lowercase()); + IdentString::new(ident) } +} + +/// Provides extra functionality on top of [`struct@Ident`]s used to name items, like fields and +/// variants. +pub(crate) trait ItemIdentExt { + /// Removes deprecation prefixed from field or variant idents. + fn as_cleaned_ident(&self) -> IdentString; +} + +impl ItemIdentExt for Ident { + fn as_cleaned_ident(&self) -> IdentString { + let ident = self.to_string(); + let ident = ident + .trim_start_matches(DEPRECATED_FIELD_PREFIX) + .trim_start_matches(DEPRECATED_VARIANT_PREFIX) + .trim_start_matches('_'); - fn as_from_impl_ident(&self) -> Ident { - format_ident!("__sv_{}", self.to_string().to_lowercase()) + IdentString::new(format_ident!("{ident}")) } } @@ -88,7 +120,7 @@ pub(crate) struct ContainerInput { pub(crate) struct VersionedContainer { /// List of declared versions for this container. Each version generates a /// definition with appropriate items. - pub(crate) versions: Vec, + pub(crate) versions: Vec, /// The original attributes that were added to the container. pub(crate) original_attributes: Vec, @@ -113,8 +145,8 @@ impl VersionedContainer { /// across structs and enums. pub(crate) fn new( input: ContainerInput, - attributes: ContainerAttributes, - versions: Vec, + attributes: StandaloneContainerAttributes, + versions: Vec, items: Vec, ) -> Self { let ContainerInput { @@ -124,11 +156,11 @@ impl VersionedContainer { } = input; let skip_from = attributes - .common_option_attrs + .common_option_args .skip .map_or(false, |s| s.from.is_present()); - let kubernetes_options = attributes.kubernetes_attrs.map(|a| KubernetesOptions { + let kubernetes_options = attributes.kubernetes_args.map(|a| KubernetesOptions { skip_merged_crd: a.skip.map_or(false, |s| s.merged_crd.is_present()), namespaced: a.namespaced.is_present(), singular: a.singular, @@ -145,7 +177,7 @@ impl VersionedContainer { let idents = VersionedContainerIdents { kubernetes: ident.as_cleaned_kubernetes_ident(), from: ident.as_from_impl_ident(), - original: ident, + original: ident.into(), }; VersionedContainer { @@ -164,13 +196,13 @@ impl VersionedContainer { pub(crate) struct VersionedContainerIdents { /// The ident used in the context of Kubernetes specific code. This ident /// removes the 'Spec' suffix present in the definition container. - pub(crate) kubernetes: Ident, + pub(crate) kubernetes: IdentString, /// The original ident, or name, of the versioned container. - pub(crate) original: Ident, + pub(crate) original: IdentString, /// The ident used in the [`From`] impl. - pub(crate) from: Ident, + pub(crate) from: IdentString, } #[derive(Debug)] diff --git a/crates/stackable-versioned-macros/src/codegen/common/item.rs b/crates/stackable-versioned-macros/src/codegen/common/item.rs index d040e637f..5463aef52 100644 --- a/crates/stackable-versioned-macros/src/codegen/common/item.rs +++ b/crates/stackable-versioned-macros/src/codegen/common/item.rs @@ -4,10 +4,10 @@ use quote::format_ident; use syn::{spanned::Spanned, Attribute, Ident, Path, Type}; use crate::{ - attrs::common::{ContainerAttributes, ItemAttributes, ValidateVersions}, + attrs::common::{ItemAttributes, StandaloneContainerAttributes, ValidateVersions}, codegen::{ chain::Neighbors, - common::{ContainerVersion, VersionChain}, + common::{VersionChain, VersionDefinition}, }, }; @@ -26,7 +26,7 @@ where /// Creates a new versioned item (struct field or enum variant) by consuming /// the parsed [Field](syn::Field) or [Variant](syn::Variant) and validating /// the versions of field actions against versions attached on the container. - fn new(item: I, container_attrs: &ContainerAttributes) -> syn::Result; + fn new(item: I, container_attrs: &StandaloneContainerAttributes) -> syn::Result; /// Inserts container versions not yet present in the status chain. /// @@ -37,10 +37,10 @@ where /// /// This continuous chain ensures that when generating code (tokens), each /// field can lookup the status (and ident) for a requested version. - fn insert_container_versions(&mut self, versions: &[ContainerVersion]); + fn insert_container_versions(&mut self, versions: &[VersionDefinition]); /// Returns the ident of the item based on the provided container version. - fn get_ident(&self, version: &ContainerVersion) -> Option<&Ident>; + fn get_ident(&self, version: &VersionDefinition) -> Option<&Ident>; } pub(crate) trait InnerItem: Named + Spanned { @@ -105,7 +105,7 @@ where A: for<'i> TryFrom<&'i I> + Attributes + ValidateVersions, I: InnerItem, { - fn new(item: I, container_attrs: &ContainerAttributes) -> syn::Result { + fn new(item: I, container_attrs: &StandaloneContainerAttributes) -> syn::Result { // We use the TryFrom trait here, because the type parameter `A` can use // it as a trait bound. Internally this then calls either `from_field` // for field attributes or `from_variant` for variant attributes. Sadly @@ -284,7 +284,7 @@ where } } - fn insert_container_versions(&mut self, versions: &[ContainerVersion]) { + fn insert_container_versions(&mut self, versions: &[VersionDefinition]) { if let Some(chain) = &mut self.chain { for version in versions { if chain.contains_key(&version.inner) { @@ -387,7 +387,7 @@ where } } - fn get_ident(&self, version: &ContainerVersion) -> Option<&Ident> { + fn get_ident(&self, version: &VersionDefinition) -> Option<&Ident> { match &self.chain { Some(chain) => chain .get(&version.inner) diff --git a/crates/stackable-versioned-macros/src/codegen/common/mod.rs b/crates/stackable-versioned-macros/src/codegen/common/mod.rs index d46cfd160..ecbc09e8a 100644 --- a/crates/stackable-versioned-macros/src/codegen/common/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/common/mod.rs @@ -6,21 +6,23 @@ use quote::format_ident; use syn::Ident; use crate::{ - attrs::common::ContainerAttributes, + attrs::common::{ModuleAttributes, StandaloneContainerAttributes}, consts::{DEPRECATED_FIELD_PREFIX, DEPRECATED_VARIANT_PREFIX}, }; mod container; mod item; +mod module; pub(crate) use container::*; pub(crate) use item::*; +pub(crate) use module::*; /// Type alias to make the type of the version chain easier to handle. pub(crate) type VersionChain = BTreeMap; #[derive(Debug, Clone)] -pub(crate) struct ContainerVersion { +pub(crate) struct VersionDefinition { /// Indicates that the container version is deprecated. pub(crate) deprecated: bool, @@ -54,17 +56,34 @@ fn process_docs(input: &Option) -> Vec { } } -impl From<&ContainerAttributes> for Vec { - fn from(attributes: &ContainerAttributes) -> Self { +// NOTE (@Techassi): Can we maybe unify these two impls? +impl From<&StandaloneContainerAttributes> for Vec { + fn from(attributes: &StandaloneContainerAttributes) -> Self { attributes .versions .iter() - .map(|v| ContainerVersion { + .map(|v| VersionDefinition { skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()), ident: Ident::new(&v.name.to_string(), Span::call_site()), + version_specific_docs: process_docs(&v.doc), deprecated: v.deprecated.is_present(), inner: v.name, + }) + .collect() + } +} + +impl From<&ModuleAttributes> for Vec { + fn from(attributes: &ModuleAttributes) -> Self { + attributes + .versions + .iter() + .map(|v| VersionDefinition { + skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()), + ident: format_ident!("{version}", version = v.name.to_string()), version_specific_docs: process_docs(&v.doc), + deprecated: v.deprecated.is_present(), + inner: v.name, }) .collect() } diff --git a/crates/stackable-versioned-macros/src/codegen/common/module.rs b/crates/stackable-versioned-macros/src/codegen/common/module.rs new file mode 100644 index 000000000..00c047aea --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/common/module.rs @@ -0,0 +1,31 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::Visibility; + +use crate::codegen::common::VersionDefinition; + +pub(crate) fn generate_module( + version: &VersionDefinition, + visibility: &Visibility, + content: TokenStream, +) -> TokenStream { + let version_ident = &version.ident; + + let deprecated_attribute = version.deprecated.then(|| { + let deprecated_note = format!("Version {version_ident} is deprecated"); + + quote! { + #[deprecated = #deprecated_note] + } + }); + + quote! { + #[automatically_derived] + #deprecated_attribute + #visibility mod #version_ident { + use super::*; + + #content + } + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/mod.rs b/crates/stackable-versioned-macros/src/codegen/mod.rs index f1b1bab79..77c617a04 100644 --- a/crates/stackable-versioned-macros/src/codegen/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/mod.rs @@ -1,59 +1,5 @@ -use proc_macro2::TokenStream; -use syn::{spanned::Spanned, Data, DeriveInput, Error, Result}; - -use crate::{ - attrs::common::ContainerAttributes, - codegen::{ - common::{Container, ContainerInput}, - venum::VersionedEnum, - vstruct::VersionedStruct, - }, -}; - pub(crate) mod chain; pub(crate) mod common; pub(crate) mod venum; +pub(crate) mod vmod; pub(crate) mod vstruct; - -// NOTE (@Techassi): This derive macro cannot handle multiple structs / enums -// to be versioned within the same file. This is because we cannot declare -// modules more than once (They will not be merged, like impl blocks for -// example). This leads to collisions if there are multiple structs / enums -// which declare the same version. This could maybe be solved by using an -// attribute macro applied to a module with all struct / enums declared in said -// module. This would allow us to generate all versioned structs and enums in -// a single sweep and put them into the appropriate module. - -// TODO (@Techassi): Think about how we can handle nested structs / enums which -// are also versioned. - -pub(crate) fn expand(attributes: ContainerAttributes, input: DeriveInput) -> Result { - let expanded = match input.data { - Data::Struct(data) => { - let input = ContainerInput { - original_attributes: input.attrs, - visibility: input.vis, - ident: input.ident, - }; - - VersionedStruct::new(input, data, attributes)?.generate_tokens() - } - Data::Enum(data) => { - let input = ContainerInput { - original_attributes: input.attrs, - visibility: input.vis, - ident: input.ident, - }; - - VersionedEnum::new(input, data, attributes)?.generate_tokens() - } - _ => { - return Err(Error::new( - input.span(), - "attribute macro `versioned` only supports structs and enums", - )) - } - }; - - Ok(expanded) -} diff --git a/crates/stackable-versioned-macros/src/codegen/venum/mod.rs b/crates/stackable-versioned-macros/src/codegen/venum/mod.rs index 14092e0b8..5690fb994 100644 --- a/crates/stackable-versioned-macros/src/codegen/venum/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/venum/mod.rs @@ -3,14 +3,14 @@ use std::ops::Deref; use itertools::Itertools; use proc_macro2::TokenStream; use quote::quote; -use syn::{DataEnum, Error}; +use syn::{punctuated::Punctuated, token::Comma, Error, Variant}; use crate::{ - attrs::common::ContainerAttributes, + attrs::common::StandaloneContainerAttributes, codegen::{ chain::Neighbors, common::{ - Container, ContainerInput, ContainerVersion, Item, ItemStatus, VersionedContainer, + Container, ContainerInput, Item, ItemStatus, VersionDefinition, VersionedContainer, }, venum::variant::VersionedVariant, }, @@ -33,11 +33,11 @@ impl Deref for VersionedEnum { } } -impl Container for VersionedEnum { +impl Container, VersionedVariant> for VersionedEnum { fn new( input: ContainerInput, - data: DataEnum, - attributes: ContainerAttributes, + variants: Punctuated, + attributes: StandaloneContainerAttributes, ) -> syn::Result { let ident = &input.ident; @@ -49,7 +49,7 @@ impl Container for VersionedEnum { // version declared by the container attribute. let mut items = Vec::new(); - for variant in data.variants { + for variant in variants { let mut versioned_field = VersionedVariant::new(variant, &attributes)?; versioned_field.insert_container_versions(&versions); items.push(versioned_field); @@ -78,7 +78,7 @@ impl Container for VersionedEnum { ))) } - fn generate_tokens(&self) -> TokenStream { + fn generate_standalone_tokens(&self) -> TokenStream { let mut token_stream = TokenStream::new(); let mut versions = self.versions.iter().peekable(); @@ -88,13 +88,17 @@ impl Container for VersionedEnum { token_stream } + + fn generate_nested_tokens(&self) -> TokenStream { + quote! {} + } } impl VersionedEnum { fn generate_version( &self, - version: &ContainerVersion, - next_version: Option<&ContainerVersion>, + version: &VersionDefinition, + next_version: Option<&VersionDefinition>, ) -> TokenStream { let mut token_stream = TokenStream::new(); @@ -143,7 +147,7 @@ impl VersionedEnum { } /// Generates version specific doc comments for the enum. - fn generate_enum_docs(&self, version: &ContainerVersion) -> TokenStream { + fn generate_enum_docs(&self, version: &VersionDefinition) -> TokenStream { let mut tokens = TokenStream::new(); for (i, doc) in version.version_specific_docs.iter().enumerate() { @@ -162,7 +166,7 @@ impl VersionedEnum { tokens } - fn generate_enum_variants(&self, version: &ContainerVersion) -> TokenStream { + fn generate_enum_variants(&self, version: &VersionDefinition) -> TokenStream { let mut token_stream = TokenStream::new(); for variant in &self.items { @@ -174,8 +178,8 @@ impl VersionedEnum { fn generate_from_impl( &self, - version: &ContainerVersion, - next_version: Option<&ContainerVersion>, + version: &VersionDefinition, + next_version: Option<&VersionDefinition>, ) -> TokenStream { if let Some(next_version) = next_version { let next_module_name = &next_version.ident; @@ -223,7 +227,7 @@ impl VersionedEnum { /// Returns whether any field is deprecated in the provided /// [`ContainerVersion`]. - fn is_any_variant_deprecated(&self, version: &ContainerVersion) -> bool { + fn is_any_variant_deprecated(&self, version: &VersionDefinition) -> bool { // First, iterate over all fields. Any will return true if any of the // function invocations return true. If a field doesn't have a chain, // we can safely default to false (unversioned fields cannot be diff --git a/crates/stackable-versioned-macros/src/codegen/venum/variant.rs b/crates/stackable-versioned-macros/src/codegen/venum/variant.rs index 5377e3ba2..81b824091 100644 --- a/crates/stackable-versioned-macros/src/codegen/venum/variant.rs +++ b/crates/stackable-versioned-macros/src/codegen/venum/variant.rs @@ -1,20 +1,20 @@ use std::ops::{Deref, DerefMut}; -use darling::FromVariant; +use darling::{util::IdentString, FromVariant}; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote}; use syn::{token::Not, Ident, Type, TypeNever, Variant}; use crate::{ attrs::{ - common::{ContainerAttributes, ItemAttributes}, + common::{ItemAttributes, StandaloneContainerAttributes}, variant::VariantAttributes, }, codegen::{ chain::BTreeMapExt, common::{ - remove_deprecated_variant_prefix, Attributes, ContainerVersion, InnerItem, Item, - ItemStatus, Named, VersionedItem, + remove_deprecated_variant_prefix, Attributes, InnerItem, Item, ItemStatus, Named, + VersionDefinition, VersionedItem, }, }, }; @@ -92,7 +92,7 @@ impl VersionedVariant { /// common creation code. pub(crate) fn new( variant: Variant, - container_attributes: &ContainerAttributes, + container_attributes: &StandaloneContainerAttributes, ) -> syn::Result { let item = VersionedItem::<_, VariantAttributes>::new(variant, container_attributes)?; Ok(Self(item)) @@ -101,7 +101,7 @@ impl VersionedVariant { /// Generates tokens to be used in a container definition. pub(crate) fn generate_for_container( &self, - container_version: &ContainerVersion, + container_version: &VersionDefinition, ) -> Option { let original_attributes = &self.original_attributes; let fields = &self.inner.fields; @@ -179,9 +179,9 @@ impl VersionedVariant { &self, module_name: &Ident, next_module_name: &Ident, - version: &ContainerVersion, - next_version: &ContainerVersion, - enum_ident: &Ident, + version: &VersionDefinition, + next_version: &VersionDefinition, + enum_ident: &IdentString, ) -> TokenStream { let variant_data = match &self.inner.fields { syn::Fields::Named(fields_named) => { diff --git a/crates/stackable-versioned-macros/src/codegen/vmod/mod.rs b/crates/stackable-versioned-macros/src/codegen/vmod/mod.rs new file mode 100644 index 000000000..c4ae62a4e --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/vmod/mod.rs @@ -0,0 +1,51 @@ +use darling::FromAttributes; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{spanned::Spanned, Error, Item, ItemMod, Result}; + +use crate::{ + attrs::common::{ModuleAttributes, NestedContainerAttributes}, + codegen::{common::VersionDefinition, venum::VersionedEnum, vstruct::VersionedStruct}, +}; + +pub(crate) struct VersionedModule { + module: ItemMod, + // TODO (@Techassi): This will change + attributes: ModuleAttributes, +} + +pub(crate) enum ModuleItem { + Struct(VersionedStruct), + Enum(VersionedEnum), +} + +impl VersionedModule { + pub(crate) fn new(module: ItemMod, attributes: ModuleAttributes) -> Result { + let versions: Vec = (&attributes).into(); + + let Some((_, items)) = &module.content else { + return Err(Error::new(module.span(), "module cannot be empty")); + }; + + // let mut versioned_items = Vec::new(); + + for item in items { + match item { + Item::Enum(item_enum) => { + let module_item_attributes = + NestedContainerAttributes::from_attributes(&item_enum.attrs)?; + // let versioned_enum = VersionedEnum::new(module_item_attributes) + // versioned_item.push(ModuleItem(versioned_enum)) + } + Item::Struct(item_struct) => todo!(), + _ => todo!(), + } + } + + Ok(VersionedModule { module, attributes }) + } + + pub(crate) fn generate_tokens(&self) -> TokenStream { + quote! {} + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/vstruct/field.rs b/crates/stackable-versioned-macros/src/codegen/vstruct/field.rs index 191303915..637c766bc 100644 --- a/crates/stackable-versioned-macros/src/codegen/vstruct/field.rs +++ b/crates/stackable-versioned-macros/src/codegen/vstruct/field.rs @@ -1,18 +1,18 @@ use std::ops::{Deref, DerefMut}; -use darling::FromField; +use darling::{util::IdentString, FromField}; use proc_macro2::TokenStream; use quote::quote; use syn::{Field, Ident}; use crate::{ attrs::{ - common::{ContainerAttributes, ItemAttributes}, + common::{ItemAttributes, StandaloneContainerAttributes}, field::FieldAttributes, }, codegen::common::{ - remove_deprecated_field_prefix, Attributes, ContainerVersion, InnerItem, Item, ItemStatus, - Named, VersionedItem, + remove_deprecated_field_prefix, Attributes, InnerItem, Item, ItemStatus, Named, + VersionDefinition, VersionedItem, }, }; @@ -89,7 +89,7 @@ impl VersionedField { /// common creation code. pub(crate) fn new( field: Field, - container_attributes: &ContainerAttributes, + container_attributes: &StandaloneContainerAttributes, ) -> syn::Result { let item = VersionedItem::<_, FieldAttributes>::new(field, container_attributes)?; Ok(Self(item)) @@ -98,7 +98,7 @@ impl VersionedField { /// Generates tokens to be used in a container definition. pub(crate) fn generate_for_container( &self, - container_version: &ContainerVersion, + container_version: &VersionDefinition, ) -> Option { let original_attributes = &self.original_attributes; @@ -192,9 +192,9 @@ impl VersionedField { /// Generates tokens to be used in a [`From`] implementation. pub(crate) fn generate_for_from_impl( &self, - version: &ContainerVersion, - next_version: &ContainerVersion, - from_ident: &Ident, + version: &VersionDefinition, + next_version: &VersionDefinition, + from_ident: &IdentString, ) -> TokenStream { match &self.chain { Some(chain) => { diff --git a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs b/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs index 6de44835f..0d9dbe076 100644 --- a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs @@ -1,17 +1,18 @@ use std::ops::Deref; +use darling::util::IdentString; use itertools::Itertools; use proc_macro2::TokenStream; use quote::quote; -use syn::{parse_quote, DataStruct, Error, Ident}; +use syn::{parse_quote, Error, Fields, Ident}; use crate::{ - attrs::common::ContainerAttributes, + attrs::common::StandaloneContainerAttributes, codegen::{ chain::Neighbors, common::{ - Container, ContainerInput, ContainerVersion, Item, ItemStatus, VersionExt, - VersionedContainer, + generate_module, Container, ContainerInput, Item, ItemStatus, VersionDefinition, + VersionExt, VersionedContainer, }, vstruct::field::VersionedField, }, @@ -19,7 +20,24 @@ use crate::{ pub(crate) mod field; -type GenerateVersionReturn = (TokenStream, Option<(TokenStream, (Ident, String))>); +// NOTE (@Techassi): The generate_version function should return a triple of values. The first +// value is the complete container definition without any module wrapping it. The second value +// contains the generated tokens for the conversion between this version and the next one. Lastly, +// the third value contains Kubernetes related code. The last top values are wrapped in Option. +// type GenerateVersionReturn = (TokenStream, Option, Option); +// type KubernetesTokens = (TokenStream, Ident, String); + +pub(crate) struct GenerateVersionTokens { + kubernetes_definition: Option, + struct_definition: TokenStream, + from_impl: Option, +} + +pub(crate) struct KubernetesTokens { + merged_crd_fn_call: TokenStream, + variant_display: String, + enum_variant: Ident, +} /// Stores individual versions of a single struct. Each version tracks field /// actions, which describe if the field was added, renamed or deprecated in @@ -36,11 +54,11 @@ impl Deref for VersionedStruct { } } -impl Container for VersionedStruct { +impl Container for VersionedStruct { fn new( input: ContainerInput, - data: DataStruct, - attributes: ContainerAttributes, + fields: Fields, + attributes: StandaloneContainerAttributes, ) -> syn::Result { let ident = &input.ident; @@ -52,7 +70,7 @@ impl Container for VersionedStruct { // version declared by the container attribute. let mut items = Vec::new(); - for field in data.fields { + for field in fields { let mut versioned_field = VersionedField::new(field, &attributes)?; versioned_field.insert_container_versions(&versions); items.push(versioned_field); @@ -78,7 +96,7 @@ impl Container for VersionedStruct { // Validate K8s specific requirements // Ensure that the struct name includes the 'Spec' suffix. - if attributes.kubernetes_attrs.is_some() && !ident.to_string().ends_with("Spec") { + if attributes.kubernetes_args.is_some() && !ident.to_string().ends_with("Spec") { return Err(Error::new( ident.span(), "struct name needs to include the `Spec` suffix if Kubernetes features are enabled via `#[versioned(k8s())]`" @@ -90,66 +108,83 @@ impl Container for VersionedStruct { ))) } - fn generate_tokens(&self) -> TokenStream { + fn generate_standalone_tokens(&self) -> TokenStream { + let mut kubernetes_definitions = Vec::new(); let mut tokens = TokenStream::new(); - let mut enum_variants = Vec::new(); - let mut crd_fn_calls = Vec::new(); - let mut versions = self.versions.iter().peekable(); while let Some(version) = versions.next() { - let (container_definition, merged_crd) = - self.generate_version(version, versions.peek().copied()); - - if let Some((crd_fn_call, enum_variant)) = merged_crd { - enum_variants.push(enum_variant); - crd_fn_calls.push(crd_fn_call); + // Generate the container definition, from implementation and Kubernetes related tokens + // for that particular version. + let GenerateVersionTokens { + struct_definition, + from_impl, + kubernetes_definition, + } = self.generate_version(version, versions.peek().copied()); + + let module_definition = generate_module(version, &self.visibility, struct_definition); + + if let Some(kubernetes_definition) = kubernetes_definition { + kubernetes_definitions.push(kubernetes_definition); } - tokens.extend(container_definition); + tokens.extend(module_definition); + tokens.extend(from_impl); } - if !crd_fn_calls.is_empty() { - tokens.extend(self.generate_kubernetes_merge_crds(crd_fn_calls, enum_variants)); + if !kubernetes_definitions.is_empty() { + tokens.extend(self.generate_kubernetes_merge_crds(kubernetes_definitions)); } tokens } + + fn generate_nested_tokens(&self) -> TokenStream { + quote! {} + } } impl VersionedStruct { /// Generates all tokens for a single instance of a versioned struct. + /// + /// This functions returns a value triple containing various pieces of generated code which can + /// be combined in multiple ways to allow generate the correct code based on which mode we are + /// running: "standalone" or "nested". + /// + /// # Struct Definition + /// + /// The first value of the triple contains the struct definition including all attributes and + /// macros it needs. These tokens **do not** include the wrapping module indicating to which + /// version this definition belongs. This is done deliberately to enable grouping multiple + /// versioned containers when running in "nested" mode. + /// + /// # From Implementation + /// + /// The second value contains the [`From`] implementation which enables conversion from _this_ + /// version to the _next_ one. These tokens need to be placed outside the version modules, + /// because they reference the structs using the version modules, like `v1alpha1` and `v1beta1`. + /// + /// # Kubernetes-specific Code + /// + /// The last value contains Kubernetes specific data. Currently, it contains data to generate + /// code to enable merging CRDs. fn generate_version( &self, - version: &ContainerVersion, - next_version: Option<&ContainerVersion>, - ) -> GenerateVersionReturn { - let mut token_stream = TokenStream::new(); - + version: &VersionDefinition, + next_version: Option<&VersionDefinition>, + ) -> GenerateVersionTokens { let original_attributes = &self.original_attributes; let struct_name = &self.idents.original; - let visibility = &self.visibility; // Generate fields of the struct for `version`. let fields = self.generate_struct_fields(version); - // TODO (@Techassi): Make the generation of the module optional to - // enable the attribute macro to be applied to a module which - // generates versioned versions of all contained containers. - - let version_ident = &version.ident; - - let deprecated_note = format!("Version {version} is deprecated", version = version_ident); - let deprecated_attr = version - .deprecated - .then_some(quote! {#[deprecated = #deprecated_note]}); - // Generate doc comments for the container (struct) let version_specific_docs = self.generate_struct_docs(version); // Generate K8s specific code - let (kubernetes_cr_derive, merged_crd) = match &self.options.kubernetes_options { + let (kubernetes_cr_derive, kubernetes_definition) = match &self.options.kubernetes_options { Some(options) => { // Generate the CustomResource derive macro with the appropriate // attributes supplied using #[kube()]. @@ -157,11 +192,15 @@ impl VersionedStruct { // Generate merged_crd specific code when not opted out. let merged_crd = if !options.skip_merged_crd { - let crd_fn_call = self.generate_kubernetes_crd_fn_call(version); + let merged_crd_fn_call = self.generate_kubernetes_crd_fn_call(version); let enum_variant = version.inner.as_variant_ident(); - let enum_display = version.inner.to_string(); + let variant_display = version.inner.to_string(); - Some((crd_fn_call, (enum_variant, enum_display))) + Some(KubernetesTokens { + merged_crd_fn_call, + variant_display, + enum_variant, + }) } else { None }; @@ -171,32 +210,32 @@ impl VersionedStruct { None => (None, None), }; - // Generate tokens for the module and the contained struct - token_stream.extend(quote! { - #[automatically_derived] - #deprecated_attr - #visibility mod #version_ident { - use super::*; - - #version_specific_docs - #(#original_attributes)* - #kubernetes_cr_derive - pub struct #struct_name { - #fields - } + // Generate struct definition tokens + let struct_definition = quote! { + #version_specific_docs + #(#original_attributes)* + #kubernetes_cr_derive + pub struct #struct_name { + #fields } - }); + }; // Generate the From impl between this `version` and the next one. - if !self.options.skip_from && !version.skip_from { - token_stream.extend(self.generate_from_impl(version, next_version)); - } + let from_impl = if !self.options.skip_from && !version.skip_from { + self.generate_from_impl(version, next_version) + } else { + None + }; - (token_stream, merged_crd) + GenerateVersionTokens { + kubernetes_definition, + struct_definition, + from_impl, + } } /// Generates version specific doc comments for the struct. - fn generate_struct_docs(&self, version: &ContainerVersion) -> TokenStream { + fn generate_struct_docs(&self, version: &VersionDefinition) -> TokenStream { let mut tokens = TokenStream::new(); for (i, doc) in version.version_specific_docs.iter().enumerate() { @@ -217,7 +256,7 @@ impl VersionedStruct { /// Generates struct fields following the `name: type` format which includes /// a trailing comma. - fn generate_struct_fields(&self, version: &ContainerVersion) -> TokenStream { + fn generate_struct_fields(&self, version: &VersionDefinition) -> TokenStream { let mut tokens = TokenStream::new(); for item in &self.items { @@ -231,8 +270,8 @@ impl VersionedStruct { /// and the next one. fn generate_from_impl( &self, - version: &ContainerVersion, - next_version: Option<&ContainerVersion>, + version: &VersionDefinition, + next_version: Option<&VersionDefinition>, ) -> Option { if let Some(next_version) = next_version { let next_module_name = &next_version.ident; @@ -272,9 +311,9 @@ impl VersionedStruct { /// `new_name: struct_name.old_name` format which includes a trailing comma. fn generate_from_fields( &self, - version: &ContainerVersion, - next_version: &ContainerVersion, - from_ident: &Ident, + version: &VersionDefinition, + next_version: &VersionDefinition, + from_ident: &IdentString, ) -> TokenStream { let mut token_stream = TokenStream::new(); @@ -287,7 +326,7 @@ impl VersionedStruct { /// Returns whether any field is deprecated in the provided /// [`ContainerVersion`]. - fn is_any_field_deprecated(&self, version: &ContainerVersion) -> bool { + fn is_any_field_deprecated(&self, version: &VersionDefinition) -> bool { // First, iterate over all fields. Any will return true if any of the // function invocations return true. If a field doesn't have a chain, // we can safely default to false (unversioned fields cannot be @@ -314,7 +353,7 @@ impl VersionedStruct { impl VersionedStruct { /// Generates the `kube::CustomResource` derive with the appropriate macro /// attributes. - fn generate_kubernetes_cr_derive(&self, version: &ContainerVersion) -> Option { + fn generate_kubernetes_cr_derive(&self, version: &VersionDefinition) -> Option { if let Some(kubernetes_options) = &self.options.kubernetes_options { // Required arguments let group = &kubernetes_options.group; @@ -349,19 +388,31 @@ impl VersionedStruct { /// Generates the `merge_crds` function call. fn generate_kubernetes_merge_crds( &self, - crd_fn_calls: Vec, - enum_variants: Vec<(Ident, String)>, + kubernetes_definitions: Vec, ) -> TokenStream { let enum_ident = &self.idents.kubernetes; let enum_vis = &self.visibility; let mut enum_display_impl_matches = TokenStream::new(); let mut enum_variant_idents = TokenStream::new(); + let mut merged_crd_fn_calls = TokenStream::new(); + + for KubernetesTokens { + merged_crd_fn_call, + variant_display, + enum_variant, + } in kubernetes_definitions + { + merged_crd_fn_calls.extend(quote! { + #merged_crd_fn_call, + }); + + enum_variant_idents.extend(quote! { + #enum_variant, + }); - for (enum_variant_ident, enum_variant_display) in enum_variants { - enum_variant_idents.extend(quote! {#enum_variant_ident,}); enum_display_impl_matches.extend(quote! { - #enum_ident::#enum_variant_ident => f.write_str(#enum_variant_display), + #enum_ident::#enum_variant => f.write_str(#variant_display), }); } @@ -386,7 +437,7 @@ impl VersionedStruct { pub fn merged_crd( stored_apiversion: Self ) -> ::std::result::Result<::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError> { - ::kube::core::crd::merge_crds(vec![#(#crd_fn_calls),*], &stored_apiversion.to_string()) + ::kube::core::crd::merge_crds(vec![#merged_crd_fn_calls], &stored_apiversion.to_string()) } /// Generates and writes a merged CRD which contains all versions defined using the `#[versioned()]` @@ -433,7 +484,7 @@ impl VersionedStruct { /// Generates the inner `crd()` functions calls which get used in the /// `merge_crds` function. - fn generate_kubernetes_crd_fn_call(&self, version: &ContainerVersion) -> TokenStream { + fn generate_kubernetes_crd_fn_call(&self, version: &VersionDefinition) -> TokenStream { let struct_ident = &self.idents.kubernetes; let version_ident = &version.ident; let path: syn::Path = parse_quote!(#version_ident::#struct_ident); diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs index 469adae82..5eaaca4d8 100644 --- a/crates/stackable-versioned-macros/src/lib.rs +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -1,8 +1,13 @@ use darling::{ast::NestedMeta, FromMeta}; use proc_macro::TokenStream; -use syn::{DeriveInput, Error}; +use syn::{spanned::Spanned, Error, Item}; -use crate::attrs::common::ContainerAttributes; +use crate::codegen::{ + common::{Container, ContainerInput}, + venum::VersionedEnum, + vmod::VersionedModule, + vstruct::VersionedStruct, +}; #[cfg(test)] mod test_utils; @@ -482,20 +487,83 @@ pub fn versioned(attrs: TokenStream, input: TokenStream) -> TokenStream { // adjustments to also support modules. One possible solution might be to // use an enum with two variants: Container(DeriveInput) and // Module(ItemMod). - let input = syn::parse_macro_input!(input as DeriveInput); + let input = syn::parse_macro_input!(input as Item); versioned_impl(attrs.into(), input).into() } -fn versioned_impl(attrs: proc_macro2::TokenStream, input: DeriveInput) -> proc_macro2::TokenStream { - let attrs = match NestedMeta::parse_meta_list(attrs) { - Ok(attrs) => match ContainerAttributes::from_list(&attrs) { - Ok(attrs) => attrs, - Err(err) => return err.write_errors(), - }, - Err(err) => return darling::Error::from(err).write_errors(), - }; +fn versioned_impl(attrs: proc_macro2::TokenStream, input: Item) -> proc_macro2::TokenStream { + // NOTE (@Techassi): This derive macro cannot handle multiple structs / enums + // to be versioned within the same file. This is because we cannot declare + // modules more than once (They will not be merged, like impl blocks for + // example). This leads to collisions if there are multiple structs / enums + // which declare the same version. This could maybe be solved by using an + // attribute macro applied to a module with all struct / enums declared in said + // module. This would allow us to generate all versioned structs and enums in + // a single sweep and put them into the appropriate module. - codegen::expand(attrs, input).unwrap_or_else(Error::into_compile_error) + // TODO (@Techassi): Think about how we can handle nested structs / enums which + // are also versioned. + + match input { + Item::Mod(item_mod) => { + let module_attributes = match parse_outer_attributes(attrs) { + Ok(ma) => ma, + Err(err) => return err.write_errors(), + }; + + match VersionedModule::new(item_mod, module_attributes) { + Ok(versioned_module) => versioned_module.generate_tokens(), + Err(err) => Error::into_compile_error(err), + } + } + Item::Enum(item_enum) => { + let container_attributes = match parse_outer_attributes(attrs) { + Ok(ca) => ca, + Err(err) => return err.write_errors(), + }; + + let input = ContainerInput { + original_attributes: item_enum.attrs, + visibility: item_enum.vis, + ident: item_enum.ident, + }; + + match VersionedEnum::new(input, item_enum.variants, container_attributes) { + Ok(versioned_enum) => versioned_enum.generate_standalone_tokens(), + Err(err) => Error::into_compile_error(err), + } + } + Item::Struct(item_struct) => { + let container_attributes = match parse_outer_attributes(attrs) { + Ok(ca) => ca, + Err(err) => return err.write_errors(), + }; + + let input = ContainerInput { + original_attributes: item_struct.attrs, + visibility: item_struct.vis, + ident: item_struct.ident, + }; + + match VersionedStruct::new(input, item_struct.fields, container_attributes) { + Ok(versioned_struct) => versioned_struct.generate_standalone_tokens(), + Err(err) => Error::into_compile_error(err), + } + } + _ => Error::new( + input.span(), + "attribute macro `versioned` can be only be applied to modules, structs and enums", + ) + .into_compile_error(), + } +} + +fn parse_outer_attributes(attrs: proc_macro2::TokenStream) -> Result +where + T: FromMeta, +{ + let nm = NestedMeta::parse_meta_list(attrs)?; + T::from_list(&nm) } #[cfg(test)] diff --git a/crates/stackable-versioned-macros/src/test_utils.rs b/crates/stackable-versioned-macros/src/test_utils.rs index 016fa9433..cc4960382 100644 --- a/crates/stackable-versioned-macros/src/test_utils.rs +++ b/crates/stackable-versioned-macros/src/test_utils.rs @@ -8,7 +8,7 @@ use insta::Settings; use proc_macro2::TokenStream; use regex::Regex; use snafu::{OptionExt, ResultExt, Snafu}; -use syn::DeriveInput; +use syn::Item; use crate::versioned_impl; @@ -50,7 +50,7 @@ pub(crate) fn expand_from_file(path: &Path) -> Result { Ok(prettyplease::unparse(&parsed)) } -fn prepare_from_string(input: String) -> Result<(TokenStream, DeriveInput), Error> { +fn prepare_from_string(input: String) -> Result<(TokenStream, Item), Error> { let (attrs, input) = input.split_once(DELIMITER).context(MissingDelimiterSnafu)?; let attrs = REGEX From 8839bc731138b1e11580f1aa13d4ef3efe791ebb Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 18 Oct 2024 09:11:58 +0200 Subject: [PATCH 04/33] refactor: Use fully qualified From path --- ...d_macros__test__default_snapshots@basic_struct.rs.snap | 8 ++++---- ...cros__test__default_snapshots@deprecate_struct.rs.snap | 8 ++++---- ...rsioned_macros__test__default_snapshots@rename.rs.snap | 4 ++-- ...acros__test__default_snapshots@skip_from_field.rs.snap | 2 +- .../stackable_versioned_macros__test__k8s_snapshots.snap | 4 ++-- .../stackable-versioned-macros/src/codegen/vstruct/mod.rs | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@basic_struct.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@basic_struct.rs.snap index 1de8b2805..98797e3e3 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@basic_struct.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@basic_struct.rs.snap @@ -15,7 +15,7 @@ pub(crate) mod v1alpha1 { } #[automatically_derived] #[allow(deprecated)] -impl From for v1beta1::Foo { +impl ::std::convert::From for v1beta1::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { Self { bar: __sv_foo.jjj.into(), @@ -33,7 +33,7 @@ pub(crate) mod v1beta1 { } } #[automatically_derived] -impl From for v1::Foo { +impl ::std::convert::From for v1::Foo { fn from(__sv_foo: v1beta1::Foo) -> Self { Self { bar: __sv_foo.bar.into(), @@ -52,7 +52,7 @@ pub(crate) mod v1 { } #[automatically_derived] #[allow(deprecated)] -impl From for v2::Foo { +impl ::std::convert::From for v2::Foo { fn from(__sv_foo: v1::Foo) -> Self { Self { deprecated_bar: __sv_foo.bar, @@ -72,7 +72,7 @@ pub(crate) mod v2 { } #[automatically_derived] #[allow(deprecated)] -impl From for v3::Foo { +impl ::std::convert::From for v3::Foo { fn from(__sv_foo: v2::Foo) -> Self { Self { deprecated_bar: __sv_foo.deprecated_bar, diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_struct.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_struct.rs.snap index afb5354a1..94d92182d 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_struct.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_struct.rs.snap @@ -12,7 +12,7 @@ mod v1alpha1 { } } #[automatically_derived] -impl From for v1beta1::Foo { +impl ::std::convert::From for v1beta1::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { Self { bar: __sv_foo.bar, @@ -30,7 +30,7 @@ mod v1beta1 { } #[automatically_derived] #[allow(deprecated)] -impl From for v1::Foo { +impl ::std::convert::From for v1::Foo { fn from(__sv_foo: v1beta1::Foo) -> Self { Self { deprecated_bar: __sv_foo.bar, @@ -49,7 +49,7 @@ mod v1 { } #[automatically_derived] #[allow(deprecated)] -impl From for v2::Foo { +impl ::std::convert::From for v2::Foo { fn from(__sv_foo: v1::Foo) -> Self { Self { deprecated_bar: __sv_foo.deprecated_bar, @@ -68,7 +68,7 @@ mod v2 { } #[automatically_derived] #[allow(deprecated)] -impl From for v3::Foo { +impl ::std::convert::From for v3::Foo { fn from(__sv_foo: v2::Foo) -> Self { Self { deprecated_bar: __sv_foo.deprecated_bar, diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@rename.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@rename.rs.snap index 6e1bc3e84..ed4443c86 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@rename.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@rename.rs.snap @@ -12,7 +12,7 @@ mod v1alpha1 { } } #[automatically_derived] -impl From for v1beta1::Foo { +impl ::std::convert::From for v1beta1::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { Self { bar: __sv_foo.bat, @@ -29,7 +29,7 @@ mod v1beta1 { } } #[automatically_derived] -impl From for v1::Foo { +impl ::std::convert::From for v1::Foo { fn from(__sv_foo: v1beta1::Foo) -> Self { Self { bar: __sv_foo.bar, diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@skip_from_field.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@skip_from_field.rs.snap index 1b40c6704..ccddd846e 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@skip_from_field.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@skip_from_field.rs.snap @@ -11,7 +11,7 @@ pub mod v1alpha1 { } } #[automatically_derived] -impl From for v1beta1::Foo { +impl ::std::convert::From for v1beta1::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { Self { bar: ::std::default::Default::default(), diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots.snap index 6aeb1cc82..f75613b5f 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots.snap @@ -21,7 +21,7 @@ pub mod v1alpha1 { } } #[automatically_derived] -impl From for v1beta1::FooSpec { +impl ::std::convert::From for v1beta1::FooSpec { fn from(__sv_foospec: v1alpha1::FooSpec) -> Self { Self { bah: ::std::default::Default::default(), @@ -48,7 +48,7 @@ pub mod v1beta1 { } } #[automatically_derived] -impl From for v1::FooSpec { +impl ::std::convert::From for v1::FooSpec { fn from(__sv_foospec: v1beta1::FooSpec) -> Self { Self { bar: __sv_foospec.bah.into(), diff --git a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs b/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs index 0d9dbe076..762900731 100644 --- a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs @@ -294,7 +294,7 @@ impl VersionedStruct { return Some(quote! { #[automatically_derived] #allow_attribute - impl From<#module_name::#struct_ident> for #next_module_name::#struct_ident { + impl ::std::convert::From<#module_name::#struct_ident> for #next_module_name::#struct_ident { fn from(#from_ident: #module_name::#struct_ident) -> Self { Self { #fields From b349321acb58ddf10925f5381ec36cf958beb678 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 18 Oct 2024 09:43:37 +0200 Subject: [PATCH 05/33] refactor: Adjust enum generation code --- ...__default_snapshots@deprecate_enum.rs.snap | 8 +- ...default_snapshots@enum_data_simple.rs.snap | 2 +- .../src/codegen/venum/mod.rs | 80 +++++++++---------- .../src/codegen/vstruct/mod.rs | 7 -- 4 files changed, 45 insertions(+), 52 deletions(-) diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_enum.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_enum.rs.snap index 17503b80c..df0e35530 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_enum.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_enum.rs.snap @@ -12,7 +12,7 @@ mod v1alpha1 { } } #[automatically_derived] -impl From for v1beta1::Foo { +impl ::std::convert::From for v1beta1::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { match __sv_foo { v1alpha1::Foo::Bar => v1beta1::Foo::Bar, @@ -30,7 +30,7 @@ mod v1beta1 { } #[automatically_derived] #[allow(deprecated)] -impl From for v1::Foo { +impl ::std::convert::From for v1::Foo { fn from(__sv_foo: v1beta1::Foo) -> Self { match __sv_foo { v1beta1::Foo::Bar => v1::Foo::DeprecatedBar, @@ -49,7 +49,7 @@ mod v1 { } #[automatically_derived] #[allow(deprecated)] -impl From for v2::Foo { +impl ::std::convert::From for v2::Foo { fn from(__sv_foo: v1::Foo) -> Self { match __sv_foo { v1::Foo::DeprecatedBar => v2::Foo::DeprecatedBar, @@ -68,7 +68,7 @@ mod v2 { } #[automatically_derived] #[allow(deprecated)] -impl From for v3::Foo { +impl ::std::convert::From for v3::Foo { fn from(__sv_foo: v2::Foo) -> Self { match __sv_foo { v2::Foo::DeprecatedBar => v3::Foo::DeprecatedBar, diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@enum_data_simple.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@enum_data_simple.rs.snap index 642b6560a..d945ef961 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@enum_data_simple.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@enum_data_simple.rs.snap @@ -12,7 +12,7 @@ mod v1alpha1 { } } #[automatically_derived] -impl From for v1alpha2::Foo { +impl ::std::convert::From for v1alpha2::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { match __sv_foo { v1alpha1::Foo::Foo => v1alpha2::Foo::Foo, diff --git a/crates/stackable-versioned-macros/src/codegen/venum/mod.rs b/crates/stackable-versioned-macros/src/codegen/venum/mod.rs index 5690fb994..4b9c5bdc3 100644 --- a/crates/stackable-versioned-macros/src/codegen/venum/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/venum/mod.rs @@ -10,7 +10,8 @@ use crate::{ codegen::{ chain::Neighbors, common::{ - Container, ContainerInput, Item, ItemStatus, VersionDefinition, VersionedContainer, + generate_module, Container, ContainerInput, Item, ItemStatus, VersionDefinition, + VersionedContainer, }, venum::variant::VersionedVariant, }, @@ -18,6 +19,11 @@ use crate::{ pub(crate) mod variant; +pub(crate) struct GenerateVersionTokens { + from_impl: Option, + enum_definition: TokenStream, +} + /// Stores individual versions of a single enum. Each version tracks variant /// actions, which describe if the variant was added, renamed or deprecated in /// that version. Variants which are not versioned, are included in every @@ -79,14 +85,22 @@ impl Container, VersionedVariant> for VersionedEnum { } fn generate_standalone_tokens(&self) -> TokenStream { - let mut token_stream = TokenStream::new(); + let mut tokens = TokenStream::new(); let mut versions = self.versions.iter().peekable(); while let Some(version) = versions.next() { - token_stream.extend(self.generate_version(version, versions.peek().copied())); + let GenerateVersionTokens { + enum_definition, + from_impl, + } = self.generate_version(version, versions.peek().copied()); + + let module_definition = generate_module(version, &self.visibility, enum_definition); + + tokens.extend(module_definition); + tokens.extend(from_impl); } - token_stream + tokens } fn generate_nested_tokens(&self) -> TokenStream { @@ -99,51 +113,37 @@ impl VersionedEnum { &self, version: &VersionDefinition, next_version: Option<&VersionDefinition>, - ) -> TokenStream { - let mut token_stream = TokenStream::new(); + ) -> GenerateVersionTokens { + let mut enum_definition = TokenStream::new(); let original_attributes = &self.original_attributes; let enum_name = &self.idents.original; - let visibility = &self.visibility; // Generate variants of the enum for `version`. let variants = self.generate_enum_variants(version); - // TODO (@Techassi): Make the generation of the module optional to - // enable the attribute macro to be applied to a module which - // generates versioned versions of all contained containers. - - let version_ident = &version.ident; - - let deprecated_note = format!("Version {version} is deprecated", version = version_ident); - let deprecated_attr = version - .deprecated - .then_some(quote! {#[deprecated = #deprecated_note]}); - // Generate doc comments for the container (enum) let version_specific_docs = self.generate_enum_docs(version); - // Generate tokens for the module and the contained enum - token_stream.extend(quote! { - #[automatically_derived] - #deprecated_attr - #visibility mod #version_ident { - use super::*; - - #version_specific_docs - #(#original_attributes)* - pub enum #enum_name { - #variants - } + // Generate enum definition tokens + enum_definition.extend(quote! { + #version_specific_docs + #(#original_attributes)* + pub enum #enum_name { + #variants } }); - // Generate the From impl between this `version` and the next one. - if !self.options.skip_from && !version.skip_from { - token_stream.extend(self.generate_from_impl(version, next_version)); - } + let from_impl = if !self.options.skip_from && !version.skip_from { + self.generate_from_impl(version, next_version) + } else { + None + }; - token_stream + GenerateVersionTokens { + enum_definition, + from_impl, + } } /// Generates version specific doc comments for the enum. @@ -180,7 +180,7 @@ impl VersionedEnum { &self, version: &VersionDefinition, next_version: Option<&VersionDefinition>, - ) -> TokenStream { + ) -> Option { if let Some(next_version) = next_version { let next_module_name = &next_version.ident; let module_name = &version.ident; @@ -209,20 +209,20 @@ impl VersionedEnum { || self.is_any_variant_deprecated(next_version)) .then_some(quote! { #[allow(deprecated)] }); - return quote! { + return Some(quote! { #[automatically_derived] #allow_attribute - impl From<#module_name::#enum_ident> for #next_module_name::#enum_ident { + impl ::std::convert::From<#module_name::#enum_ident> for #next_module_name::#enum_ident { fn from(#from_ident: #module_name::#enum_ident) -> Self { match #from_ident { #variants } } } - }; + }); } - quote! {} + None } /// Returns whether any field is deprecated in the provided diff --git a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs b/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs index 762900731..d4b966087 100644 --- a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs @@ -20,13 +20,6 @@ use crate::{ pub(crate) mod field; -// NOTE (@Techassi): The generate_version function should return a triple of values. The first -// value is the complete container definition without any module wrapping it. The second value -// contains the generated tokens for the conversion between this version and the next one. Lastly, -// the third value contains Kubernetes related code. The last top values are wrapped in Option. -// type GenerateVersionReturn = (TokenStream, Option, Option); -// type KubernetesTokens = (TokenStream, Ident, String); - pub(crate) struct GenerateVersionTokens { kubernetes_definition: Option, struct_definition: TokenStream, From a922c605caced54de713de12739312b30d83340c Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 13 Nov 2024 17:11:31 +0100 Subject: [PATCH 06/33] feat: Add support for modules This commit contains a major rework of most parts of the attribute macro to support attaching it to modules. Most of the previously existing features are already in place. There are only two missing features which will be added in follow-up commits: version specific doc comments and merged CRD YAML support. Because of that, some snapshot tests are failing. --- .../src/attrs/common.rs | 94 ++++ .../src/attrs/common/container.rs | 154 ------ .../src/attrs/common/item.rs | 375 -------------- .../src/attrs/common/mod.rs | 40 -- .../src/attrs/common/module.rs | 30 -- .../src/attrs/container.rs | 54 ++ .../src/attrs/{ => item}/field.rs | 17 +- .../src/attrs/item/mod.rs | 448 ++++++++++++++++ .../src/attrs/{ => item}/variant.rs | 17 +- .../src/attrs/{common => }/k8s.rs | 0 .../src/attrs/mod.rs | 6 +- .../src/attrs/module.rs | 10 + .../src/codegen/chain.rs | 113 ---- .../src/codegen/changes.rs | 223 ++++++++ .../src/codegen/common/container.rs | 222 -------- .../src/codegen/common/item.rs | 439 ---------------- .../src/codegen/common/mod.rs | 116 ----- .../src/codegen/common/module.rs | 31 -- .../src/codegen/container/enum.rs | 191 +++++++ .../src/codegen/container/mod.rs | 218 ++++++++ .../src/codegen/container/struct.rs | 330 ++++++++++++ .../src/codegen/{vstruct => item}/field.rs | 174 +++---- .../src/codegen/item/mod.rs | 5 + .../src/codegen/item/variant.rs | 215 ++++++++ .../src/codegen/mod.rs | 146 +++++- .../src/codegen/module.rs | 92 ++++ .../src/codegen/venum/mod.rs | 251 --------- .../src/codegen/venum/variant.rs | 246 --------- .../src/codegen/vmod/mod.rs | 51 -- .../src/codegen/vstruct/mod.rs | 489 ------------------ .../stackable-versioned-macros/src/consts.rs | 2 - crates/stackable-versioned-macros/src/lib.rs | 103 ++-- .../stackable-versioned-macros/src/utils.rs | 117 +++++ rust-toolchain.toml | 2 +- 34 files changed, 2289 insertions(+), 2732 deletions(-) create mode 100644 crates/stackable-versioned-macros/src/attrs/common.rs delete mode 100644 crates/stackable-versioned-macros/src/attrs/common/container.rs delete mode 100644 crates/stackable-versioned-macros/src/attrs/common/item.rs delete mode 100644 crates/stackable-versioned-macros/src/attrs/common/mod.rs delete mode 100644 crates/stackable-versioned-macros/src/attrs/common/module.rs create mode 100644 crates/stackable-versioned-macros/src/attrs/container.rs rename crates/stackable-versioned-macros/src/attrs/{ => item}/field.rs (78%) create mode 100644 crates/stackable-versioned-macros/src/attrs/item/mod.rs rename crates/stackable-versioned-macros/src/attrs/{ => item}/variant.rs (82%) rename crates/stackable-versioned-macros/src/attrs/{common => }/k8s.rs (100%) create mode 100644 crates/stackable-versioned-macros/src/attrs/module.rs delete mode 100644 crates/stackable-versioned-macros/src/codegen/chain.rs create mode 100644 crates/stackable-versioned-macros/src/codegen/changes.rs delete mode 100644 crates/stackable-versioned-macros/src/codegen/common/container.rs delete mode 100644 crates/stackable-versioned-macros/src/codegen/common/item.rs delete mode 100644 crates/stackable-versioned-macros/src/codegen/common/mod.rs delete mode 100644 crates/stackable-versioned-macros/src/codegen/common/module.rs create mode 100644 crates/stackable-versioned-macros/src/codegen/container/enum.rs create mode 100644 crates/stackable-versioned-macros/src/codegen/container/mod.rs create mode 100644 crates/stackable-versioned-macros/src/codegen/container/struct.rs rename crates/stackable-versioned-macros/src/codegen/{vstruct => item}/field.rs (56%) create mode 100644 crates/stackable-versioned-macros/src/codegen/item/mod.rs create mode 100644 crates/stackable-versioned-macros/src/codegen/item/variant.rs create mode 100644 crates/stackable-versioned-macros/src/codegen/module.rs delete mode 100644 crates/stackable-versioned-macros/src/codegen/venum/mod.rs delete mode 100644 crates/stackable-versioned-macros/src/codegen/venum/variant.rs delete mode 100644 crates/stackable-versioned-macros/src/codegen/vmod/mod.rs delete mode 100644 crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs delete mode 100644 crates/stackable-versioned-macros/src/consts.rs create mode 100644 crates/stackable-versioned-macros/src/utils.rs diff --git a/crates/stackable-versioned-macros/src/attrs/common.rs b/crates/stackable-versioned-macros/src/attrs/common.rs new file mode 100644 index 000000000..c243f0d1a --- /dev/null +++ b/crates/stackable-versioned-macros/src/attrs/common.rs @@ -0,0 +1,94 @@ +use darling::{ + util::{Flag, SpannedValue}, + Error, FromMeta, Result, +}; +use itertools::Itertools; +use k8s_version::Version; + +#[derive(Debug, FromMeta)] +#[darling(and_then = CommonRootArguments::validate)] +pub(crate) struct CommonRootArguments { + #[darling(default)] + pub(crate) options: RootOptions, + + #[darling(multiple, rename = "version")] + pub(crate) versions: SpannedValue>, +} + +impl CommonRootArguments { + fn validate(mut self) -> Result { + let mut errors = Error::accumulator(); + + if self.versions.is_empty() { + errors.push( + Error::custom("at least one or more `version`s must be defined") + .with_span(&self.versions.span()), + ); + } + + let is_sorted = self.versions.iter().is_sorted_by_key(|v| v.name); + + // It needs to be sorted, even tho the definition could be unsorted (if allow_unsorted is + // set). + self.versions.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); + + if !self.options.allow_unsorted.is_present() && !is_sorted { + let versions = self.versions.iter().map(|v| v.name).join(", "); + + errors.push(Error::custom(format!( + "versions must be defined in ascending order: {versions}", + ))); + } + + let duplicate_versions: Vec<_> = self + .versions + .iter() + .duplicates_by(|v| v.name) + .map(|v| v.name) + .collect(); + + if !duplicate_versions.is_empty() { + let versions = duplicate_versions.iter().join(", "); + + errors.push(Error::custom(format!( + "contains duplicate versions: {versions}", + ))); + } + + errors.finish_with(self) + } +} + +#[derive(Clone, Debug, Default, FromMeta)] +pub(crate) struct RootOptions { + pub(crate) allow_unsorted: Flag, + pub(crate) skip: Option, +} + +/// This struct contains supported version arguments. +/// +/// Supported arguments are: +/// +/// - `name` of the version, like `v1alpha1`. +/// - `deprecated` flag to mark that version as deprecated. +/// - `skip` option to skip generating various pieces of code. +/// - `doc` option to add version-specific documentation. +#[derive(Clone, Debug, FromMeta)] +pub(crate) struct VersionArguments { + pub(crate) deprecated: Flag, + pub(crate) name: Version, + pub(crate) skip: Option, + pub(crate) doc: Option, +} + +/// This struct contains supported common skip arguments. +/// +/// Supported arguments are: +/// +/// - `from` flag, which skips generating [`From`] implementations when provided. +#[derive(Clone, Debug, Default, FromMeta)] +pub(crate) struct SkipArguments { + /// Whether the [`From`] implementation generation should be skipped for all versions of this + /// container. + pub(crate) from: Flag, +} diff --git a/crates/stackable-versioned-macros/src/attrs/common/container.rs b/crates/stackable-versioned-macros/src/attrs/common/container.rs deleted file mode 100644 index 42f942e27..000000000 --- a/crates/stackable-versioned-macros/src/attrs/common/container.rs +++ /dev/null @@ -1,154 +0,0 @@ -//! This module contains attributes which can be used on containers (structs and enums). -//! -//! Generally there are two different containers, called "standalone" and "nested" based on which -//! context they are used in. Standalone containers define versioning directly on the struct or -//! enum. This is useful for single versioned containers. This type of versioning is fine as long -//! as there is no other versioned container in the same file using the same versions. If that is -//! the case, the generated modules will collide. There are two possible solutions for this: move -//! each versioned container into its own file or use the nested declarations. It should be noted -//! that there might be cases where it is fine to separate each container into its own file. One -//! such case is when each container serves distinctively different use-cases and provide numerous -//! associated items like functions. -//! -//! In cases where separate files are not desired, the nested mode can be used. The nested mode -//! allows to declare versions on a module which contains containers which shall be versioned -//! according to the defined versions. This approach allows defining multiple versioned containers -//! in the same file without module collisions. -//! -//! The attributes used must be tailored to both of these two modes, because not all arguments are -//! valid in all modes. As such different attributes allow different validation mechanisms. One -//! such an example is that nested containers must not define versions as the definition is done -//! on the module. This is in direct contrast to containers used in standalone mode. - -use std::{cmp::Ordering, ops::Deref}; - -use darling::{ - util::{Flag, SpannedValue}, - Error, FromAttributes, FromMeta, Result, -}; -use itertools::Itertools; - -use crate::attrs::common::{KubernetesArguments, SkipArguments, VersionArguments}; - -/// This struct contains supported container attributes which can be applied to structs and enums. -/// -/// Currently supported attributes are: -/// -/// - `version`, which can occur one or more times. See [`VersionAttributes`]. -/// - `k8s`, which enables Kubernetes specific features and allows customization if these features. -/// - `options`, which allow further customization of the generated code. -/// See [`StandaloneOptionArguments`]. -#[derive(Debug, FromMeta)] -#[darling(and_then = StandaloneContainerAttributes::validate)] -pub(crate) struct StandaloneContainerAttributes { - #[darling(multiple, rename = "version")] - pub(crate) versions: SpannedValue>, - - #[darling(rename = "k8s")] - pub(crate) kubernetes_args: Option, - - #[darling(default, rename = "options")] - pub(crate) common_option_args: StandaloneOptionArguments, -} - -impl StandaloneContainerAttributes { - fn validate(mut self) -> Result { - // Most of the validation for individual version strings is done by the - // k8s-version crate. That's why the code below only checks that at - // least one version is defined, they are defined in order (to ensure - // code consistency) and that all declared versions are unique. - - // If there are no versions defined, the derive macro errors out. There - // should be at least one version if the derive macro is used. - if self.versions.is_empty() { - return Err(Error::custom( - "attribute macro `#[versioned()]` must contain at least one `version`", - ) - .with_span(&self.versions.span())); - } - - // NOTE (@Techassi): Do we even want to allow to opt-out of this? - - // Ensure that versions are defined in sorted (ascending) order to keep - // code consistent. - if !self.common_option_args.allow_unsorted.is_present() { - let original = self.versions.deref().clone(); - self.versions - .sort_by(|lhs, rhs| lhs.name.partial_cmp(&rhs.name).unwrap_or(Ordering::Equal)); - - for (index, version) in original.iter().enumerate() { - if version.name - == self - .versions - .get(index) - .expect("internal error: version at that index must exist") - .name - { - continue; - } - - return Err(Error::custom(format!( - "versions in `#[versioned()]` must be defined in ascending order (version `{name}` is misplaced)", - name = version.name - ))); - } - } - - // TODO (@Techassi): Add validation for skip(from) for last version, - // which will skip nothing, because nothing is generated in the first - // place. - - // Ensure every version is unique and isn't declared multiple times. - let duplicate_versions = self - .versions - .iter() - .duplicates_by(|e| e.name) - .map(|e| e.name) - .join(", "); - - if !duplicate_versions.is_empty() { - return Err(Error::custom(format!( - "attribute macro `#[versioned()]` contains duplicate versions: {duplicate_versions}", - )) - .with_span(&self.versions.span())); - } - - // Ensure that the 'k8s' feature is enabled when the 'k8s()' - // attribute is used. - if self.kubernetes_args.is_some() && cfg!(not(feature = "k8s")) { - return Err(Error::custom( - "the `#[versioned(k8s())]` attribute can only be used when the `k8s` feature is enabled", - )); - } - - Ok(self) - } -} - -/// This struct contains supported option arguments for containers used in standalone mode. -/// -/// Supported arguments are: -/// -/// - `allow_unsorted`, which allows declaring versions in unsorted order, instead of enforcing -/// ascending order. -/// - `skip` option to skip generating various pieces of code. -#[derive(Clone, Debug, Default, FromMeta)] -pub(crate) struct StandaloneOptionArguments { - pub(crate) allow_unsorted: Flag, - pub(crate) skip: Option, -} - -#[derive(Debug, FromAttributes)] -#[darling(attributes(versioned))] -pub(crate) struct NestedContainerAttributes { - #[darling(rename = "k8s")] - pub(crate) kubernetes_args: Option, - - #[darling(default, rename = "options")] - pub(crate) common_option_args: NestedOptionArguments, -} - -#[derive(Clone, Debug, Default, FromMeta)] -pub(crate) struct NestedOptionArguments { - pub(crate) skip: Option, -} diff --git a/crates/stackable-versioned-macros/src/attrs/common/item.rs b/crates/stackable-versioned-macros/src/attrs/common/item.rs deleted file mode 100644 index 5c7c51b21..000000000 --- a/crates/stackable-versioned-macros/src/attrs/common/item.rs +++ /dev/null @@ -1,375 +0,0 @@ -use darling::{util::SpannedValue, Error, FromMeta}; -use k8s_version::Version; -use proc_macro2::Span; -use syn::{spanned::Spanned, Attribute, Ident, Path, Type}; - -use crate::{ - attrs::common::StandaloneContainerAttributes, - codegen::common::Attributes, - consts::{DEPRECATED_FIELD_PREFIX, DEPRECATED_VARIANT_PREFIX}, -}; - -/// This trait helps to unify attribute validation for both field and variant -/// attributes. -/// -/// This trait is implemented using a blanket implementation on types -/// `T: Attributes`. The [`Attributes`] trait allows access to the common -/// attributes shared across field and variant attributes. -pub(crate) trait ValidateVersions -where - I: Spanned, -{ - /// Validates that each field or variant action version is present in the - /// declared container versions. - fn validate_versions( - &self, - container_attrs: &StandaloneContainerAttributes, - item: &I, - ) -> Result<(), darling::Error>; -} - -impl ValidateVersions for T -where - T: Attributes, - I: Spanned, -{ - fn validate_versions( - &self, - container_attrs: &StandaloneContainerAttributes, - item: &I, - ) -> Result<(), darling::Error> { - // NOTE (@Techassi): Can we maybe optimize this a little? - - let mut errors = Error::accumulator(); - - if let Some(added) = &self.common_attributes().added { - if !container_attrs - .versions - .iter() - .any(|v| v.name == *added.since) - { - errors.push(Error::custom( - "variant action `added` uses version which was not declared via #[versioned(version)]") - .with_span(item) - ); - } - } - - for change in &*self.common_attributes().changes { - if !container_attrs - .versions - .iter() - .any(|v| v.name == *change.since) - { - errors.push( - Error::custom("variant action `changed` uses version which was not declared via #[versioned(version)]") - .with_span(item) - ); - } - } - - if let Some(deprecated) = &self.common_attributes().deprecated { - if !container_attrs - .versions - .iter() - .any(|v| v.name == *deprecated.since) - { - errors.push(Error::custom( - "variant action `deprecated` uses version which was not declared via #[versioned(version)]") - .with_span(item) - ); - } - } - - errors.finish()?; - Ok(()) - } -} - -// NOTE (@Techassi): It might be possible (but is it required) to move this -// functionality into a shared trait, which knows what type of item 'Self' is. - -/// This enum is used to run different validation based on the type of item. -#[derive(Debug, strum::Display)] -#[strum(serialize_all = "lowercase")] -pub(crate) enum ItemType { - Field, - Variant, -} - -// TODO (@Techassi): Shower thought: Track actions as a Vec of an ActionAttribute -// enum and implement Ord on it. This would allow us to order the items by type -// of action (added < changed < deprecated) as well as sort changed action to -// each other by specified version (which already implements Ord) - -/// These attributes are meant to be used in super structs, which add -/// [`Field`](syn::Field) or [`Variant`](syn::Variant) specific attributes via -/// darling's flatten feature. This struct only provides shared attributes. -/// -/// ### Shared Item Rules -/// -/// - An item can only ever be added once at most. An item not marked as 'added' -/// is part of the container in every version until changed or deprecated. -/// - An item can be changed many times. That's why changes are stored in a -/// [`Vec`]. -/// - An item can only be deprecated once. A field or variant not marked as -/// 'deprecated' will be included up until the latest version. -#[derive(Debug, FromMeta)] -pub(crate) struct ItemAttributes { - /// This parses the `added` attribute on items (fields or variants). It can - /// only be present at most once. - pub(crate) added: Option, - - /// This parses the `changed` attribute on items (fields or variants). It - /// can be present 0..n times. - #[darling(multiple, rename = "changed")] - pub(crate) changes: Vec, - - /// This parses the `deprecated` attribute on items (fields or variants). It - /// can only be present at most once. - pub(crate) deprecated: Option, -} - -impl ItemAttributes { - pub(crate) fn validate( - &self, - item_ident: &Ident, - item_type: &ItemType, - item_attrs: &Vec, - ) -> Result<(), Error> { - // NOTE (@Techassi): This associated function is NOT called by darling's - // and_then attribute, but instead by the wrapper, FieldAttributes and - // VariantAttributes. - - let mut errors = Error::accumulator(); - - // Semantic validation - errors.handle(self.validate_action_combinations(item_ident, item_type)); - errors.handle(self.validate_action_order(item_ident, item_type)); - errors.handle(self.validate_item_name(item_ident, item_type)); - errors.handle(self.validate_changed_item_name(item_type)); - errors.handle(self.validate_item_attributes(item_attrs)); - - // TODO (@Techassi): Add hint if a field or variant is added in the - // first version that it might be clever to remove the 'added' - // attribute. - - errors.finish()?; - - Ok(()) - } - - /// This associated function is called by the top-level validation function - /// and validates that each item uses a valid combination of actions. - /// Invalid combinations are: - /// - /// - `added` and `deprecated` using the same version: A field or variant - /// cannot be marked as added in a particular version and then marked as - /// deprecated immediately after. Fields and variants must be included for - /// at least one version before being marked deprecated. - /// - `added` and `changed` using the same version: The same reasoning from - /// above applies here as well. Fields and variants must be included for - /// at least one version before being changed. - /// - `changed` and `deprecated` using the same version: Again, the same - /// rules from above apply here as well. - fn validate_action_combinations( - &self, - item_ident: &Ident, - item_type: &ItemType, - ) -> Result<(), Error> { - match (&self.added, &self.changes, &self.deprecated) { - (Some(added), _, Some(deprecated)) if *added.since == *deprecated.since => { - Err(Error::custom(format!( - "{item_type} cannot be marked as `added` and `deprecated` in the same version" - )) - .with_span(item_ident)) - } - (Some(added), changed, _) if changed.iter().any(|r| *r.since == *added.since) => { - Err(Error::custom(format!( - "{item_type} cannot be marked as `added` and `changed` in the same version" - )) - .with_span(item_ident)) - } - (_, changed, Some(deprecated)) - if changed.iter().any(|r| *r.since == *deprecated.since) => - { - Err(Error::custom( - format!("{item_type} cannot be marked as `deprecated` and `changed` in the same version"), - ) - .with_span(item_ident)) - } - _ => Ok(()), - } - } - - /// This associated function is called by the top-level validation function - /// and validates that actions use a chronologically sound chain of - /// versions. - /// - /// The following rules apply: - /// - /// - `deprecated` must use a greater version than `added`: This function - /// ensures that these versions are chronologically sound, that means, - /// that the version of the deprecated action must be greater than the - /// version of the added action. - /// - All `changed` actions must use a greater version than `added` but a - /// lesser version than `deprecated`. - fn validate_action_order(&self, item_ident: &Ident, item_type: &ItemType) -> Result<(), Error> { - let added_version = self.added.as_ref().map(|a| *a.since); - let deprecated_version = self.deprecated.as_ref().map(|d| *d.since); - - // First, validate that the added version is less than the deprecated - // version. - // NOTE (@Techassi): Is this already covered by the code below? - if let (Some(added_version), Some(deprecated_version)) = (added_version, deprecated_version) - { - if added_version > deprecated_version { - return Err(Error::custom(format!( - "{item_type} was marked as `added` in version `{added_version}` while being marked as `deprecated` in an earlier version `{deprecated_version}`" - )).with_span(item_ident)); - } - } - - // Now, iterate over all changes and ensure that their versions are - // between the added and deprecated version. - if !self.changes.iter().all(|r| { - added_version.map_or(true, |a| a < *r.since) - && deprecated_version.map_or(true, |d| d > *r.since) - }) { - return Err(Error::custom( - "all changes must use versions higher than `added` and lower than `deprecated`", - ) - .with_span(item_ident)); - } - - Ok(()) - } - - /// This associated function is called by the top-level validation function - /// and validates that items use correct names depending on attached - /// actions. - /// - /// The following naming rules apply: - /// - /// - Fields or variants marked as deprecated need to include the - /// deprecation prefix in their name. The prefix must not be included for - /// fields or variants which are not deprecated. - fn validate_item_name(&self, item_ident: &Ident, item_type: &ItemType) -> Result<(), Error> { - let prefix = match item_type { - ItemType::Field => DEPRECATED_FIELD_PREFIX, - ItemType::Variant => DEPRECATED_VARIANT_PREFIX, - }; - - let starts_with_deprecated = item_ident.to_string().starts_with(prefix); - - if self.deprecated.is_some() && !starts_with_deprecated { - return Err(Error::custom( - format!("{item_type} was marked as `deprecated` and thus must include the `{prefix}` prefix in its name") - ).with_span(item_ident)); - } - - if self.deprecated.is_none() && starts_with_deprecated { - return Err(Error::custom( - format!("{item_type} includes the `{prefix}` prefix in its name but is not marked as `deprecated`") - ).with_span(item_ident)); - } - - Ok(()) - } - - /// This associated function is called by the top-level validation function - /// and validates that disallowed item attributes are not used. - /// - /// The following naming rules apply: - /// - /// - `deprecated` must not be set on items. Instead, use the `deprecated()` - /// action of the `#[versioned()]` macro. - fn validate_item_attributes(&self, item_attrs: &Vec) -> Result<(), Error> { - for attr in item_attrs { - for segment in &attr.path().segments { - if segment.ident == "deprecated" { - return Err(Error::custom("deprecation must be done using #[versioned(deprecated(since = \"VERSION\"))]") - .with_span(&attr.span())); - } - } - } - Ok(()) - } - - /// This associated function is called by the top-level validation function - /// and validates that parameters provided to the `changed` actions are - /// valid. - fn validate_changed_item_name(&self, item_type: &ItemType) -> Result<(), Error> { - let prefix = match item_type { - ItemType::Field => DEPRECATED_FIELD_PREFIX, - ItemType::Variant => DEPRECATED_VARIANT_PREFIX, - }; - - let mut errors = Error::accumulator(); - - // This ensures that `from_name` doesn't include the deprecation prefix. - for change in &self.changes { - if let Some(from_name) = change.from_name.as_ref() { - if from_name.starts_with(prefix) { - errors.push( - Error::custom(format!( - "the previous {item_type} name must not start with the deprecation prefix" - )) - .with_span(&from_name.span()), - ); - } - } - } - - errors.finish() - } -} - -// TODO (@Techassi): Add validation for when default_fn is "" (empty path). -/// For the added() action -/// -/// Example usage: -/// - `added(since = "...")` -/// - `added(since = "...", default_fn = "custom_fn")` -#[derive(Clone, Debug, FromMeta)] -pub(crate) struct AddedAttributes { - pub(crate) since: SpannedValue, - - #[darling(rename = "default", default = "default_default_fn")] - pub(crate) default_fn: SpannedValue, -} - -fn default_default_fn() -> SpannedValue { - SpannedValue::new( - syn::parse_str("::std::default::Default::default") - .expect("internal error: path must parse"), - Span::call_site(), - ) -} - -// TODO (@Techassi): Add validation for when from_name AND from_type are both -// none => is this action needed in the first place? -// TODO (@Techassi): Add validation that the from_name mustn't include the -// deprecated prefix. -/// For the changed() action -/// -/// Example usage: -/// - `changed(since = "...", from_name = "...")` -/// - `changed(since = "...", from_name = "..." from_type="...")` -#[derive(Clone, Debug, FromMeta)] -pub(crate) struct ChangedAttributes { - pub(crate) since: SpannedValue, - pub(crate) from_name: Option>, - pub(crate) from_type: Option>, -} - -/// For the deprecated() action -/// -/// Example usage: -/// - `deprecated(since = "...")` -/// - `deprecated(since = "...", note = "...")` -#[derive(Clone, Debug, FromMeta)] -pub(crate) struct DeprecatedAttributes { - pub(crate) since: SpannedValue, - pub(crate) note: Option>, -} diff --git a/crates/stackable-versioned-macros/src/attrs/common/mod.rs b/crates/stackable-versioned-macros/src/attrs/common/mod.rs deleted file mode 100644 index 7c232273f..000000000 --- a/crates/stackable-versioned-macros/src/attrs/common/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -use darling::{util::Flag, FromMeta}; -use k8s_version::Version; - -mod container; -mod item; -mod k8s; -mod module; - -pub(crate) use container::*; -pub(crate) use item::*; -pub(crate) use k8s::*; -pub(crate) use module::*; - -/// This struct contains supported version arguments. -/// -/// Supported arguments are: -/// -/// - `name` of the version, like `v1alpha1`. -/// - `deprecated` flag to mark that version as deprecated. -/// - `skip` option to skip generating various pieces of code. -/// - `doc` option to add version-specific documentation. -#[derive(Clone, Debug, FromMeta)] -pub(crate) struct VersionArguments { - pub(crate) deprecated: Flag, - pub(crate) name: Version, - pub(crate) skip: Option, - pub(crate) doc: Option, -} - -/// This struct contains supported common skip arguments. -/// -/// Supported arguments are: -/// -/// - `from` flag, which skips generating [`From`] implementations when provided. -#[derive(Clone, Debug, Default, FromMeta)] -pub(crate) struct SkipArguments { - /// Whether the [`From`] implementation generation should be skipped for all versions of this - /// container. - pub(crate) from: Flag, -} diff --git a/crates/stackable-versioned-macros/src/attrs/common/module.rs b/crates/stackable-versioned-macros/src/attrs/common/module.rs deleted file mode 100644 index 5d0f48268..000000000 --- a/crates/stackable-versioned-macros/src/attrs/common/module.rs +++ /dev/null @@ -1,30 +0,0 @@ -use darling::{ - util::{Flag, SpannedValue}, - FromMeta, Result, -}; - -use crate::attrs::common::{SkipArguments, VersionArguments}; - -#[derive(Debug, FromMeta)] -#[darling(and_then = ModuleAttributes::validate)] -pub(crate) struct ModuleAttributes { - #[darling(multiple, rename = "version")] - pub(crate) versions: SpannedValue>, - - #[darling(default, rename = "options")] - pub(crate) common_option_args: ModuleOptionArguments, -} - -impl ModuleAttributes { - fn validate(self) -> Result { - // TODO (@Techassi): Make this actually validate - Ok(self) - } -} - -#[derive(Debug, Default, FromMeta)] -pub(crate) struct ModuleOptionArguments { - pub(crate) skip: Option, - pub(crate) preserve_module: Flag, - pub(crate) allow_unsorted: Flag, -} diff --git a/crates/stackable-versioned-macros/src/attrs/container.rs b/crates/stackable-versioned-macros/src/attrs/container.rs new file mode 100644 index 000000000..f9af53c25 --- /dev/null +++ b/crates/stackable-versioned-macros/src/attrs/container.rs @@ -0,0 +1,54 @@ +use darling::{Error, FromAttributes, FromMeta, Result}; + +use crate::attrs::{ + common::{CommonRootArguments, SkipArguments}, + k8s::KubernetesArguments, +}; + +#[derive(Debug, FromMeta)] +#[darling(and_then = StandaloneContainerAttributes::validate)] +pub(crate) struct StandaloneContainerAttributes { + #[darling(rename = "k8s")] + pub(crate) kubernetes_arguments: Option, + + #[darling(flatten)] + pub(crate) common_root_arguments: CommonRootArguments, +} + +impl StandaloneContainerAttributes { + fn validate(self) -> Result { + if self.kubernetes_arguments.is_some() && cfg!(not(feature = "k8s")) { + return Err(Error::custom("the `#[versioned(k8s())]` attribute can only be used when the `k8s` feature is enabled")); + } + + Ok(self) + } +} + +#[derive(Debug, FromAttributes)] +#[darling( + attributes(versioned), + and_then = NestedContainerAttributes::validate +)] +pub(crate) struct NestedContainerAttributes { + #[darling(rename = "k8s")] + pub(crate) kubernetes_arguments: Option, + + #[darling(default)] + pub(crate) options: NestedContainerOptionArguments, +} + +impl NestedContainerAttributes { + fn validate(self) -> Result { + if self.kubernetes_arguments.is_some() && cfg!(not(feature = "k8s")) { + return Err(Error::custom("the `#[versioned(k8s())]` attribute can only be used when the `k8s` feature is enabled")); + } + + Ok(self) + } +} + +#[derive(Debug, Default, FromMeta)] +pub(crate) struct NestedContainerOptionArguments { + pub(crate) skip: Option, +} diff --git a/crates/stackable-versioned-macros/src/attrs/field.rs b/crates/stackable-versioned-macros/src/attrs/item/field.rs similarity index 78% rename from crates/stackable-versioned-macros/src/attrs/field.rs rename to crates/stackable-versioned-macros/src/attrs/item/field.rs index d5c9aa9ff..1d45688b0 100644 --- a/crates/stackable-versioned-macros/src/attrs/field.rs +++ b/crates/stackable-versioned-macros/src/attrs/item/field.rs @@ -1,7 +1,7 @@ -use darling::{Error, FromField}; +use darling::{FromField, Result}; use syn::{Attribute, Ident}; -use crate::attrs::common::{ItemAttributes, ItemType}; +use crate::{attrs::item::CommonItemAttributes, codegen::VersionDefinition, utils::FieldIdent}; /// This struct describes all available field attributes, as well as the field /// name to display better diagnostics. @@ -24,7 +24,7 @@ use crate::attrs::common::{ItemAttributes, ItemType}; )] pub(crate) struct FieldAttributes { #[darling(flatten)] - pub(crate) common: ItemAttributes, + pub(crate) common: CommonItemAttributes, // The ident (automatically extracted by darling) cannot be moved into the // shared item attributes because for struct fields, the type is @@ -45,13 +45,18 @@ impl FieldAttributes { /// place by darling. /// /// Internally, it calls out to other specialized validation functions. - fn validate(self) -> Result { + fn validate(self) -> Result { let ident = self .ident .as_ref() - .expect("internal error: field must have an ident"); + .expect("internal error: field must have an ident") + .clone(); - self.common.validate(ident, &ItemType::Field, &self.attrs)?; + self.common.validate(FieldIdent::from(ident), &self.attrs)?; Ok(self) } + + pub(crate) fn validate_versions(&self, versions: &[VersionDefinition]) -> Result<()> { + self.common.validate_versions(versions) + } } diff --git a/crates/stackable-versioned-macros/src/attrs/item/mod.rs b/crates/stackable-versioned-macros/src/attrs/item/mod.rs new file mode 100644 index 000000000..7b2b59256 --- /dev/null +++ b/crates/stackable-versioned-macros/src/attrs/item/mod.rs @@ -0,0 +1,448 @@ +use std::{collections::BTreeMap, ops::Deref}; + +use darling::{util::SpannedValue, Error, FromMeta, Result}; +use k8s_version::Version; +use proc_macro2::Span; +use quote::format_ident; +use syn::{spanned::Spanned, Attribute, Path, Type}; + +use crate::{ + codegen::{ItemStatus, VersionDefinition}, + utils::ItemIdentExt, +}; + +mod field; +pub(crate) use field::*; + +mod variant; +pub(crate) use variant::*; + +/// These attributes are meant to be used in super structs, which add +/// [`Field`](syn::Field) or [`Variant`](syn::Variant) specific attributes via +/// darling's flatten feature. This struct only provides shared attributes. +/// +/// ### Shared Item Rules +/// +/// - An item can only ever be added once at most. An item not marked as 'added' +/// is part of the container in every version until changed or deprecated. +/// - An item can be changed many times. That's why changes are stored in a +/// [`Vec`]. +/// - An item can only be deprecated once. A field or variant not marked as +/// 'deprecated' will be included up until the latest version. +#[derive(Debug, FromMeta)] +pub(crate) struct CommonItemAttributes { + /// This parses the `added` attribute on items (fields or variants). It can + /// only be present at most once. + pub(crate) added: Option, + + /// This parses the `changed` attribute on items (fields or variants). It + /// can be present 0..n times. + #[darling(multiple, rename = "changed")] + pub(crate) changes: Vec, + + /// This parses the `deprecated` attribute on items (fields or variants). It + /// can only be present at most once. + pub(crate) deprecated: Option, +} + +// This impl block ONLY contains validation. The main entrypoint is the associated 'validate' +// function. In addition to validate functions which are called directly during darling's parsing, +// it contains functions which can only be called after the initial parsing and validation because +// they need additional context, namely the list of versions defined on the container or module. +impl CommonItemAttributes { + pub(crate) fn validate( + &self, + item_ident: impl ItemIdentExt, + item_attrs: &[Attribute], + ) -> Result<()> { + let mut errors = Error::accumulator(); + + errors.handle(self.validate_action_combinations(&item_ident)); + errors.handle(self.validate_action_order(&item_ident)); + errors.handle(self.validate_item_name(&item_ident)); + errors.handle(self.validate_added_action()); + errors.handle(self.validate_changed_action(&item_ident)); + errors.handle(self.validate_item_attributes(item_attrs)); + + errors.finish() + } + + pub(crate) fn validate_versions(&self, versions: &[VersionDefinition]) -> Result<()> { + let mut errors = Error::accumulator(); + + if let Some(added) = &self.added { + if !versions.iter().any(|v| v.inner == *added.since) { + errors.push(Error::custom( + "the `added` action uses a version which is not declared via `#[versioned(version)]`", + ).with_span(&added.since.span())); + } + } + + for change in &self.changes { + if !versions.iter().any(|v| v.inner == *change.since) { + errors.push(Error::custom( + "the `changed` action uses a version which is not declared via `#[versioned(version)]`" + ).with_span(&change.since.span())); + } + } + + if let Some(deprecated) = &self.deprecated { + if !versions.iter().any(|v| v.inner == *deprecated.since) { + errors.push(Error::custom( + "the `deprecated` action uses a version which is not declared via `#[versioned(version)]`", + ).with_span(&deprecated.since.span())); + } + } + + errors.finish() + } + + /// This associated function is called by the top-level validation function + /// and validates that each item uses a valid combination of actions. + /// Invalid combinations are: + /// + /// - `added` and `deprecated` using the same version: A field or variant + /// cannot be marked as added in a particular version and then marked as + /// deprecated immediately after. Fields and variants must be included for + /// at least one version before being marked deprecated. + /// - `added` and `changed` using the same version: The same reasoning from + /// above applies here as well. Fields and variants must be included for + /// at least one version before being changed. + /// - `changed` and `deprecated` using the same version: Again, the same + /// rules from above apply here as well. + fn validate_action_combinations(&self, item_ident: &impl ItemIdentExt) -> Result<()> { + match (&self.added, &self.changes, &self.deprecated) { + (Some(added), _, Some(deprecated)) if *added.since == *deprecated.since => Err( + Error::custom("cannot be marked as `added` and `deprecated` in the same version") + .with_span(item_ident), + ), + (Some(added), changed, _) if changed.iter().any(|r| *r.since == *added.since) => Err( + Error::custom("cannot be marked as `added` and `changed` in the same version") + .with_span(item_ident), + ), + (_, changed, Some(deprecated)) + if changed.iter().any(|r| *r.since == *deprecated.since) => + { + Err(Error::custom( + "cannot be marked as `deprecated` and `changed` in the same version", + ) + .with_span(item_ident)) + } + _ => Ok(()), + } + } + + /// This associated function is called by the top-level validation function + /// and validates that actions use a chronologically sound chain of + /// versions. + /// + /// The following rules apply: + /// + /// - `deprecated` must use a greater version than `added`: This function + /// ensures that these versions are chronologically sound, that means, + /// that the version of the deprecated action must be greater than the + /// version of the added action. + /// - All `changed` actions must use a greater version than `added` but a + /// lesser version than `deprecated`. + fn validate_action_order(&self, item_ident: &impl ItemIdentExt) -> Result<()> { + let added_version = self.added.as_ref().map(|a| *a.since); + let deprecated_version = self.deprecated.as_ref().map(|d| *d.since); + + // First, validate that the added version is less than the deprecated + // version. + // NOTE (@Techassi): Is this already covered by the code below? + if let (Some(added_version), Some(deprecated_version)) = (added_version, deprecated_version) + { + if added_version > deprecated_version { + return Err(Error::custom(format!( + "cannot marked as `added` in version `{added_version}` while being marked as `deprecated` in an earlier version `{deprecated_version}`" + )).with_span(item_ident)); + } + } + + // Now, iterate over all changes and ensure that their versions are + // between the added and deprecated version. + if !self.changes.iter().all(|r| { + added_version.map_or(true, |a| a < *r.since) + && deprecated_version.map_or(true, |d| d > *r.since) + }) { + return Err(Error::custom( + "all changes must use versions higher than `added` and lower than `deprecated`", + ) + .with_span(item_ident)); + } + + Ok(()) + } + + /// This associated function is called by the top-level validation function + /// and validates that items use correct names depending on attached + /// actions. + /// + /// The following naming rules apply: + /// + /// - Fields or variants marked as deprecated need to include the + /// deprecation prefix in their name. The prefix must not be included for + /// fields or variants which are not deprecated. + fn validate_item_name(&self, item_ident: &impl ItemIdentExt) -> Result<()> { + let starts_with_deprecated = item_ident.starts_with_deprecated_prefix(); + + if self.deprecated.is_some() && !starts_with_deprecated { + return Err(Error::custom(format!( + "marked as `deprecated` and thus must include the `{deprecated_prefix}` prefix", + deprecated_prefix = item_ident.deprecated_prefix() + )) + .with_span(item_ident)); + } + + if self.deprecated.is_none() && starts_with_deprecated { + return Err(Error::custom( + format!("not marked as `deprecated` and thus mustn't include the `{deprecated_prefix}` prefix", deprecated_prefix = item_ident.deprecated_prefix()) + ).with_span(item_ident)); + } + + Ok(()) + } + + fn validate_added_action(&self) -> Result<()> { + // NOTE (@Techassi): Can the path actually be empty? + if let Some(added) = &self.added { + if added.default_fn.segments.is_empty() { + return Err(Error::custom("`default_fn` cannot be empty") + .with_span(&added.default_fn.span())); + } + } + + Ok(()) + } + + /// This associated function is called by the top-level validation function + /// and validates that parameters provided to the `changed` actions are + /// valid. + fn validate_changed_action(&self, item_ident: &impl ItemIdentExt) -> Result<()> { + let mut errors = Error::accumulator(); + + // This ensures that `from_name` doesn't include the deprecation prefix. + for change in &self.changes { + if let Some(from_name) = change.from_name.as_ref() { + if from_name.starts_with(item_ident.deprecated_prefix()) { + errors.push( + Error::custom( + "the previous name mustn't start with the deprecation prefix", + ) + .with_span(&from_name.span()), + ); + } + } + } + + errors.finish() + } + + /// This associated function is called by the top-level validation function + /// and validates that disallowed item attributes are not used. + /// + /// The following naming rules apply: + /// + /// - `deprecated` must not be set on items. Instead, use the `deprecated()` + /// action of the `#[versioned()]` macro. + fn validate_item_attributes(&self, item_attrs: &[Attribute]) -> Result<()> { + for attr in item_attrs { + for segment in &attr.path().segments { + if segment.ident == "deprecated" { + return Err(Error::custom("deprecation must be done using `#[versioned(deprecated(since = \"VERSION\"))]`") + .with_span(&attr.span())); + } + } + } + Ok(()) + } +} + +impl CommonItemAttributes { + pub(crate) fn into_changeset( + self, + ident: &impl ItemIdentExt, + ty: Type, + ) -> Option> { + // TODO (@Techassi): Use Change instead of ItemStatus + if let Some(deprecated) = self.deprecated { + let deprecated_ident = ident.deref(); + + // When the item is deprecated, any change which occurred beforehand + // requires access to the item ident to infer the item ident for + // the latest change. + let mut ident = ident.as_cleaned_ident(); + let mut ty = ty; + + let mut actions = BTreeMap::new(); + + actions.insert( + *deprecated.since, + ItemStatus::Deprecation { + previous_ident: ident.clone(), + ident: deprecated_ident.clone(), + note: deprecated.note.as_deref().cloned(), + }, + ); + + for change in self.changes.iter().rev() { + let from_ident = if let Some(from) = change.from_name.as_deref() { + format_ident!("{from}").into() + } else { + ident.clone() + }; + + // TODO (@Techassi): This is an awful lot of cloning, can we get + // rid of it? + let from_ty = change + .from_type + .as_ref() + .map(|sv| sv.deref().clone()) + .unwrap_or(ty.clone()); + + actions.insert( + *change.since, + ItemStatus::Change { + from_ident: from_ident.clone(), + to_ident: ident, + from_type: from_ty.clone(), + to_type: ty, + }, + ); + + ident = from_ident; + ty = from_ty; + } + + // After the last iteration above (if any) we use the ident for the + // added action if there is any. + if let Some(added) = self.added { + actions.insert( + *added.since, + ItemStatus::Addition { + default_fn: added.default_fn.deref().clone(), + ident, + ty, + }, + ); + } + + Some(actions) + } else if !self.changes.is_empty() { + let mut ident = ident.deref().clone(); + let mut ty = ty; + + let mut actions = BTreeMap::new(); + + for change in self.changes.iter().rev() { + let from_ident = if let Some(from) = change.from_name.as_deref() { + format_ident!("{from}").into() + } else { + ident.clone() + }; + + // TODO (@Techassi): This is an awful lot of cloning, can we get + // rid of it? + let from_ty = change + .from_type + .as_ref() + .map(|sv| sv.deref().clone()) + .unwrap_or(ty.clone()); + + actions.insert( + *change.since, + ItemStatus::Change { + from_ident: from_ident.clone(), + to_ident: ident, + from_type: from_ty.clone(), + to_type: ty, + }, + ); + + ident = from_ident; + ty = from_ty; + } + + // After the last iteration above (if any) we use the ident for the + // added action if there is any. + if let Some(added) = self.added { + actions.insert( + *added.since, + ItemStatus::Addition { + default_fn: added.default_fn.deref().clone(), + ident, + ty, + }, + ); + } + + Some(actions) + } else { + if let Some(added) = self.added { + let mut actions = BTreeMap::new(); + + actions.insert( + *added.since, + ItemStatus::Addition { + default_fn: added.default_fn.deref().clone(), + ident: ident.deref().clone(), + ty, + }, + ); + + return Some(actions); + } + + None + } + } +} + +/// For the added() action +/// +/// Example usage: +/// - `added(since = "...")` +/// - `added(since = "...", default_fn = "custom_fn")` +#[derive(Clone, Debug, FromMeta)] +pub(crate) struct AddedAttributes { + pub(crate) since: SpannedValue, + + #[darling(rename = "default", default = "default_default_fn")] + pub(crate) default_fn: SpannedValue, +} + +fn default_default_fn() -> SpannedValue { + SpannedValue::new( + syn::parse_str("::std::default::Default::default") + .expect("internal error: path must parse"), + Span::call_site(), + ) +} + +// TODO (@Techassi): Add validation for when from_name AND from_type are both +// none => is this action needed in the first place? +// TODO (@Techassi): Add validation that the from_name mustn't include the +// deprecated prefix. +/// For the changed() action +/// +/// Example usage: +/// - `changed(since = "...", from_name = "...")` +/// - `changed(since = "...", from_name = "..." from_type="...")` +#[derive(Clone, Debug, FromMeta)] +pub(crate) struct ChangedAttributes { + pub(crate) since: SpannedValue, + pub(crate) from_name: Option>, + pub(crate) from_type: Option>, +} + +/// For the deprecated() action +/// +/// Example usage: +/// - `deprecated(since = "...")` +/// - `deprecated(since = "...", note = "...")` +#[derive(Clone, Debug, FromMeta)] +pub(crate) struct DeprecatedAttributes { + pub(crate) since: SpannedValue, + pub(crate) note: Option>, +} diff --git a/crates/stackable-versioned-macros/src/attrs/variant.rs b/crates/stackable-versioned-macros/src/attrs/item/variant.rs similarity index 82% rename from crates/stackable-versioned-macros/src/attrs/variant.rs rename to crates/stackable-versioned-macros/src/attrs/item/variant.rs index bcdde256a..3cf2cb038 100644 --- a/crates/stackable-versioned-macros/src/attrs/variant.rs +++ b/crates/stackable-versioned-macros/src/attrs/item/variant.rs @@ -1,8 +1,8 @@ use convert_case::{Case, Casing}; -use darling::{Error, FromVariant}; +use darling::{Error, FromVariant, Result}; use syn::{Attribute, Ident}; -use crate::attrs::common::{ItemAttributes, ItemType}; +use crate::{attrs::item::CommonItemAttributes, codegen::VersionDefinition, utils::VariantIdent}; /// This struct describes all available variant attributes, as well as the /// variant name to display better diagnostics. @@ -25,7 +25,7 @@ use crate::attrs::common::{ItemAttributes, ItemType}; )] pub(crate) struct VariantAttributes { #[darling(flatten)] - pub(crate) common: ItemAttributes, + pub(crate) common: CommonItemAttributes, // The ident (automatically extracted by darling) cannot be moved into the // shared item attributes because for struct fields, the type is @@ -46,12 +46,12 @@ impl VariantAttributes { /// place by darling. /// /// Internally, it calls out to other specialized validation functions. - fn validate(self) -> Result { + fn validate(self) -> Result { let mut errors = Error::accumulator(); errors.handle( self.common - .validate(&self.ident, &ItemType::Variant, &self.attrs), + .validate(VariantIdent::from(self.ident.clone()), &self.attrs), ); // Validate names of renames @@ -66,7 +66,10 @@ impl VariantAttributes { } } - errors.finish()?; - Ok(self) + errors.finish_with(self) + } + + pub(crate) fn validate_versions(&self, versions: &[VersionDefinition]) -> Result<()> { + self.common.validate_versions(versions) } } diff --git a/crates/stackable-versioned-macros/src/attrs/common/k8s.rs b/crates/stackable-versioned-macros/src/attrs/k8s.rs similarity index 100% rename from crates/stackable-versioned-macros/src/attrs/common/k8s.rs rename to crates/stackable-versioned-macros/src/attrs/k8s.rs diff --git a/crates/stackable-versioned-macros/src/attrs/mod.rs b/crates/stackable-versioned-macros/src/attrs/mod.rs index 6eb6b96bd..e7c06ef8d 100644 --- a/crates/stackable-versioned-macros/src/attrs/mod.rs +++ b/crates/stackable-versioned-macros/src/attrs/mod.rs @@ -1,3 +1,5 @@ pub(crate) mod common; -pub(crate) mod field; -pub(crate) mod variant; +pub(crate) mod container; +pub(crate) mod item; +pub(crate) mod k8s; +pub(crate) mod module; diff --git a/crates/stackable-versioned-macros/src/attrs/module.rs b/crates/stackable-versioned-macros/src/attrs/module.rs new file mode 100644 index 000000000..b6c3b5ee4 --- /dev/null +++ b/crates/stackable-versioned-macros/src/attrs/module.rs @@ -0,0 +1,10 @@ +use darling::{util::Flag, FromMeta}; + +use crate::attrs::common::CommonRootArguments; + +#[derive(Debug, FromMeta)] +pub(crate) struct ModuleAttributes { + #[darling(flatten)] + pub(crate) common_root_arguments: CommonRootArguments, + pub(crate) preserve_module: Flag, +} diff --git a/crates/stackable-versioned-macros/src/codegen/chain.rs b/crates/stackable-versioned-macros/src/codegen/chain.rs deleted file mode 100644 index 2376e3ffd..000000000 --- a/crates/stackable-versioned-macros/src/codegen/chain.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::{collections::BTreeMap, ops::Bound}; - -pub(crate) trait Neighbors -where - K: Ord + Eq, -{ - /// Returns the values of keys which are neighbors of `key`. - /// - /// Given a map which contains the following keys: 1, 3, 5. Calling this - /// function with these keys, results in the following return values: - /// - /// - Key **0**: `(None, Some(1))` - /// - Key **2**: `(Some(1), Some(3))` - /// - Key **4**: `(Some(3), Some(5))` - /// - Key **6**: `(Some(5), None)` - fn get_neighbors(&self, key: &K) -> (Option<&V>, Option<&V>); - - /// Returns whether the function `f` returns true if applied to the value - /// identified by `key`. - fn value_is(&self, key: &K, f: F) -> bool - where - F: Fn(&V) -> bool; - - fn lo_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)>; - fn up_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)>; -} - -impl Neighbors for BTreeMap -where - K: Ord + Eq, -{ - fn get_neighbors(&self, key: &K) -> (Option<&V>, Option<&V>) { - // NOTE (@Techassi): These functions might get added to the standard - // library at some point. If that's the case, we can use the ones - // provided by the standard lib. - // See: https://github.com/rust-lang/rust/issues/107540 - match ( - self.lo_bound(Bound::Excluded(key)), - self.up_bound(Bound::Excluded(key)), - ) { - (Some((k, v)), None) => { - if key > k { - (Some(v), None) - } else { - (self.lo_bound(Bound::Excluded(k)).map(|(_, v)| v), None) - } - } - (None, Some((k, v))) => { - if key < k { - (None, Some(v)) - } else { - (None, self.up_bound(Bound::Excluded(k)).map(|(_, v)| v)) - } - } - (Some((_, lo)), Some((_, up))) => (Some(lo), Some(up)), - (None, None) => unreachable!(), - } - } - - fn value_is(&self, key: &K, f: F) -> bool - where - F: Fn(&V) -> bool, - { - self.get(key).map_or(false, f) - } - - fn lo_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)> { - self.range((Bound::Unbounded, bound)).next_back() - } - - fn up_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)> { - self.range((bound, Bound::Unbounded)).next() - } -} - -pub(crate) trait BTreeMapExt -where - K: Ord, -{ - const MESSAGE: &'static str; - - fn get_expect(&self, key: &K) -> &V; -} - -impl BTreeMapExt for BTreeMap -where - K: Ord, -{ - const MESSAGE: &'static str = "internal error: chain must contain version"; - - fn get_expect(&self, key: &K) -> &V { - self.get(key).expect(Self::MESSAGE) - } -} - -#[cfg(test)] -mod test { - use super::*; - use rstest::rstest; - - #[rstest] - #[case(0, (None, Some(&"test1")))] - #[case(1, (None, Some(&"test3")))] - #[case(2, (Some(&"test1"), Some(&"test3")))] - #[case(3, (Some(&"test1"), None))] - #[case(4, (Some(&"test3"), None))] - fn neighbors(#[case] key: i32, #[case] expected: (Option<&&str>, Option<&&str>)) { - let map = BTreeMap::from([(1, "test1"), (3, "test3")]); - let neigbors = map.get_neighbors(&key); - - assert_eq!(neigbors, expected); - } -} diff --git a/crates/stackable-versioned-macros/src/codegen/changes.rs b/crates/stackable-versioned-macros/src/codegen/changes.rs new file mode 100644 index 000000000..80f3849e1 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/changes.rs @@ -0,0 +1,223 @@ +use std::{collections::BTreeMap, ops::Bound}; + +use k8s_version::Version; +use syn::Type; + +use crate::codegen::{ItemStatus, VersionDefinition}; + +pub(crate) trait Neighbors +where + K: Ord + Eq, +{ + /// Returns the values of keys which are neighbors of `key`. + /// + /// Given a map which contains the following keys: 1, 3, 5. Calling this + /// function with these keys, results in the following return values: + /// + /// - Key **0**: `(None, Some(1))` + /// - Key **2**: `(Some(1), Some(3))` + /// - Key **4**: `(Some(3), Some(5))` + /// - Key **6**: `(Some(5), None)` + fn get_neighbors(&self, key: &K) -> (Option<&V>, Option<&V>); + + /// Returns whether the function `f` returns true if applied to the value + /// identified by `key`. + fn value_is(&self, key: &K, f: F) -> bool + where + F: Fn(&V) -> bool; + + fn lo_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)>; + fn up_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)>; +} + +impl Neighbors for BTreeMap +where + K: Ord + Eq, +{ + fn get_neighbors(&self, key: &K) -> (Option<&V>, Option<&V>) { + // NOTE (@Techassi): These functions might get added to the standard + // library at some point. If that's the case, we can use the ones + // provided by the standard lib. + // See: https://github.com/rust-lang/rust/issues/107540 + match ( + self.lo_bound(Bound::Excluded(key)), + self.up_bound(Bound::Excluded(key)), + ) { + (Some((k, v)), None) => { + if key > k { + (Some(v), None) + } else { + (self.lo_bound(Bound::Excluded(k)).map(|(_, v)| v), None) + } + } + (None, Some((k, v))) => { + if key < k { + (None, Some(v)) + } else { + (None, self.up_bound(Bound::Excluded(k)).map(|(_, v)| v)) + } + } + (Some((_, lo)), Some((_, up))) => (Some(lo), Some(up)), + (None, None) => unreachable!(), + } + } + + fn value_is(&self, key: &K, f: F) -> bool + where + F: Fn(&V) -> bool, + { + self.get(key).map_or(false, f) + } + + fn lo_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)> { + self.range((Bound::Unbounded, bound)).next_back() + } + + fn up_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)> { + self.range((bound, Bound::Unbounded)).next() + } +} + +pub(crate) trait BTreeMapExt +where + K: Ord, +{ + const MESSAGE: &'static str; + + fn get_expect(&self, key: &K) -> &V; +} + +impl BTreeMapExt for BTreeMap +where + K: Ord, +{ + const MESSAGE: &'static str = "internal error: chain must contain version"; + + fn get_expect(&self, key: &K) -> &V { + self.get(key).expect(Self::MESSAGE) + } +} + +pub(crate) trait ChangesetExt { + fn insert_container_versions(&mut self, versions: &[VersionDefinition], ty: &Type); +} + +impl ChangesetExt for BTreeMap { + fn insert_container_versions(&mut self, versions: &[VersionDefinition], ty: &Type) { + for version in versions { + if self.contains_key(&version.inner) { + continue; + } + + match self.get_neighbors(&version.inner) { + (None, Some(status)) => match status { + ItemStatus::Addition { .. } => { + self.insert(version.inner, ItemStatus::NotPresent) + } + ItemStatus::Change { + from_ident, + from_type, + .. + } => self.insert( + version.inner, + ItemStatus::NoChange { + previously_deprecated: false, + ident: from_ident.clone(), + ty: from_type.clone(), + }, + ), + ItemStatus::Deprecation { previous_ident, .. } => self.insert( + version.inner, + ItemStatus::NoChange { + previously_deprecated: false, + ident: previous_ident.clone(), + ty: ty.clone(), + }, + ), + ItemStatus::NoChange { + previously_deprecated, + ident, + ty, + } => self.insert( + version.inner, + ItemStatus::NoChange { + previously_deprecated: *previously_deprecated, + ident: ident.clone(), + ty: ty.clone(), + }, + ), + ItemStatus::NotPresent => unreachable!(), + }, + (Some(status), None) => { + let (ident, ty, previously_deprecated) = match status { + ItemStatus::Addition { ident, ty, .. } => (ident, ty, false), + ItemStatus::Change { + to_ident, to_type, .. + } => (to_ident, to_type, false), + ItemStatus::Deprecation { ident, .. } => (ident, ty, true), + ItemStatus::NoChange { + previously_deprecated, + ident, + ty, + .. + } => (ident, ty, *previously_deprecated), + ItemStatus::NotPresent => unreachable!(), + }; + + self.insert( + version.inner, + ItemStatus::NoChange { + previously_deprecated, + ident: ident.clone(), + ty: ty.clone(), + }, + ) + } + (Some(status), Some(_)) => { + let (ident, ty, previously_deprecated) = match status { + ItemStatus::Addition { ident, ty, .. } => (ident, ty, false), + ItemStatus::Change { + to_ident, to_type, .. + } => (to_ident, to_type, false), + ItemStatus::NoChange { + previously_deprecated, + ident, + ty, + .. + } => (ident, ty, *previously_deprecated), + _ => unreachable!(), + }; + + self.insert( + version.inner, + ItemStatus::NoChange { + previously_deprecated, + ident: ident.clone(), + ty: ty.clone(), + }, + ) + } + _ => unreachable!(), + }; + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use rstest::rstest; + + #[rstest] + #[case(0, (None, Some(&"test1")))] + #[case(1, (None, Some(&"test3")))] + #[case(2, (Some(&"test1"), Some(&"test3")))] + #[case(3, (Some(&"test1"), None))] + #[case(4, (Some(&"test3"), None))] + fn neighbors(#[case] key: i32, #[case] expected: (Option<&&str>, Option<&&str>)) { + let map = BTreeMap::from([(1, "test1"), (3, "test3")]); + let neigbors = map.get_neighbors(&key); + + assert_eq!(neigbors, expected); + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/common/container.rs b/crates/stackable-versioned-macros/src/codegen/common/container.rs deleted file mode 100644 index 40b8d0a0f..000000000 --- a/crates/stackable-versioned-macros/src/codegen/common/container.rs +++ /dev/null @@ -1,222 +0,0 @@ -use std::ops::Deref; - -use convert_case::{Case, Casing}; -use darling::util::IdentString; -use k8s_version::Version; -use proc_macro2::TokenStream; -use quote::format_ident; -use syn::{Attribute, Ident, Visibility}; - -use crate::{ - attrs::common::StandaloneContainerAttributes, - codegen::common::VersionDefinition, - consts::{DEPRECATED_FIELD_PREFIX, DEPRECATED_VARIANT_PREFIX}, -}; - -/// This trait helps to unify versioned containers, like structs and enums. -/// -/// This trait is implemented by wrapper structs, which wrap the generic -/// [`VersionedContainer`] struct. The generic type parameter `D` describes the -/// kind of data, like [`DataStruct`](syn::DataStruct) in case of a struct and -/// [`DataEnum`](syn::DataEnum) in case of an enum. -/// The type parameter `I` describes the type of the versioned items, like -/// [`VersionedField`][1] and [`VersionedVariant`][2]. -/// -/// [1]: crate::codegen::vstruct::field::VersionedField -/// [2]: crate::codegen::venum::variant::VersionedVariant -pub(crate) trait Container -where - Self: Sized + Deref>, -{ - /// Creates a new versioned container. - fn new( - input: ContainerInput, - data: D, - attributes: StandaloneContainerAttributes, - ) -> syn::Result; - - /// This generates the complete code for a single versioned container. - /// - /// Internally, it will create a module for each declared version which - /// contains the container with the appropriate items (fields or variants) - /// Additionally, it generates `From` implementations, which enable - /// conversion from an older to a newer version. - fn generate_standalone_tokens(&self) -> TokenStream; - - fn generate_nested_tokens(&self) -> TokenStream; -} - -/// Provides extra functionality on top of [`struct@Ident`]s used to name containers. -pub(crate) trait ContainerIdentExt { - /// Removes the 'Spec' suffix from the [`struct@Ident`]. - fn as_cleaned_kubernetes_ident(&self) -> IdentString; - - /// Transforms the [`struct@Ident`] into one usable in the [`From`] impl. - fn as_from_impl_ident(&self) -> IdentString; -} - -impl ContainerIdentExt for Ident { - fn as_cleaned_kubernetes_ident(&self) -> IdentString { - let ident = format_ident!("{}", self.to_string().trim_end_matches("Spec")); - IdentString::new(ident) - } - - fn as_from_impl_ident(&self) -> IdentString { - let ident = format_ident!("__sv_{}", self.to_string().to_lowercase()); - IdentString::new(ident) - } -} - -/// Provides extra functionality on top of [`struct@Ident`]s used to name items, like fields and -/// variants. -pub(crate) trait ItemIdentExt { - /// Removes deprecation prefixed from field or variant idents. - fn as_cleaned_ident(&self) -> IdentString; -} - -impl ItemIdentExt for Ident { - fn as_cleaned_ident(&self) -> IdentString { - let ident = self.to_string(); - let ident = ident - .trim_start_matches(DEPRECATED_FIELD_PREFIX) - .trim_start_matches(DEPRECATED_VARIANT_PREFIX) - .trim_start_matches('_'); - - IdentString::new(format_ident!("{ident}")) - } -} - -pub(crate) trait VersionExt { - fn as_variant_ident(&self) -> Ident; -} - -impl VersionExt for Version { - fn as_variant_ident(&self) -> Ident { - format_ident!("{ident}", ident = self.to_string().to_case(Case::Pascal)) - } -} - -/// This struct bundles values from [`DeriveInput`][1]. -/// -/// [`DeriveInput`][1] cannot be used directly when constructing a -/// [`VersionedStruct`][2] or [`VersionedEnum`][3] because we run into borrow -/// issues caused by the match statement which extracts the data. -/// -/// [1]: syn::DeriveInput -/// [2]: crate::codegen::vstruct::VersionedStruct -/// [3]: crate::codegen::venum::VersionedEnum -pub(crate) struct ContainerInput { - pub(crate) original_attributes: Vec, - pub(crate) visibility: Visibility, - pub(crate) ident: Ident, -} - -/// Stores individual versions of a single container. -/// -/// Each version tracks item actions, which describe if the item was added, -/// renamed or deprecated in that particular version. Items which are not -/// versioned are included in every version of the container. -#[derive(Debug)] -pub(crate) struct VersionedContainer { - /// List of declared versions for this container. Each version generates a - /// definition with appropriate items. - pub(crate) versions: Vec, - - /// The original attributes that were added to the container. - pub(crate) original_attributes: Vec, - - /// The visibility of the versioned container. Used to forward the - /// visibility during code generation. - pub(crate) visibility: Visibility, - - /// List of items defined in the original container. How, and if, an item - /// should generate code, is decided by the currently generated version. - pub(crate) items: Vec, - - /// Different options which influence code generation. - pub(crate) options: VersionedContainerOptions, - - /// A collection of container idents used for different purposes. - pub(crate) idents: VersionedContainerIdents, -} - -impl VersionedContainer { - /// Creates a new versioned Container which contains common data shared - /// across structs and enums. - pub(crate) fn new( - input: ContainerInput, - attributes: StandaloneContainerAttributes, - versions: Vec, - items: Vec, - ) -> Self { - let ContainerInput { - original_attributes, - visibility, - ident, - } = input; - - let skip_from = attributes - .common_option_args - .skip - .map_or(false, |s| s.from.is_present()); - - let kubernetes_options = attributes.kubernetes_args.map(|a| KubernetesOptions { - skip_merged_crd: a.skip.map_or(false, |s| s.merged_crd.is_present()), - namespaced: a.namespaced.is_present(), - singular: a.singular, - plural: a.plural, - group: a.group, - kind: a.kind, - }); - - let options = VersionedContainerOptions { - kubernetes_options, - skip_from, - }; - - let idents = VersionedContainerIdents { - kubernetes: ident.as_cleaned_kubernetes_ident(), - from: ident.as_from_impl_ident(), - original: ident.into(), - }; - - VersionedContainer { - original_attributes, - visibility, - versions, - options, - idents, - items, - } - } -} - -/// A collection of container idents used for different purposes. -#[derive(Debug)] -pub(crate) struct VersionedContainerIdents { - /// The ident used in the context of Kubernetes specific code. This ident - /// removes the 'Spec' suffix present in the definition container. - pub(crate) kubernetes: IdentString, - - /// The original ident, or name, of the versioned container. - pub(crate) original: IdentString, - - /// The ident used in the [`From`] impl. - pub(crate) from: IdentString, -} - -#[derive(Debug)] -pub(crate) struct VersionedContainerOptions { - pub(crate) kubernetes_options: Option, - pub(crate) skip_from: bool, -} - -#[derive(Debug)] -pub(crate) struct KubernetesOptions { - pub(crate) singular: Option, - pub(crate) plural: Option, - pub(crate) skip_merged_crd: bool, - pub(crate) kind: Option, - pub(crate) namespaced: bool, - pub(crate) group: String, -} diff --git a/crates/stackable-versioned-macros/src/codegen/common/item.rs b/crates/stackable-versioned-macros/src/codegen/common/item.rs deleted file mode 100644 index 5463aef52..000000000 --- a/crates/stackable-versioned-macros/src/codegen/common/item.rs +++ /dev/null @@ -1,439 +0,0 @@ -use std::{collections::BTreeMap, marker::PhantomData, ops::Deref}; - -use quote::format_ident; -use syn::{spanned::Spanned, Attribute, Ident, Path, Type}; - -use crate::{ - attrs::common::{ItemAttributes, StandaloneContainerAttributes, ValidateVersions}, - codegen::{ - chain::Neighbors, - common::{VersionChain, VersionDefinition}, - }, -}; - -/// This trait describes versioned container items, fields and variants in a -/// common way. -/// -/// Shared functionality is implemented in a single place. Code which cannot be -/// shared is implemented on the wrapping type, like [`VersionedField`][1]. -/// -/// [1]: crate::codegen::vstruct::field::VersionedField -pub(crate) trait Item: Sized -where - A: for<'i> TryFrom<&'i I> + Attributes, - I: InnerItem, -{ - /// Creates a new versioned item (struct field or enum variant) by consuming - /// the parsed [Field](syn::Field) or [Variant](syn::Variant) and validating - /// the versions of field actions against versions attached on the container. - fn new(item: I, container_attrs: &StandaloneContainerAttributes) -> syn::Result; - - /// Inserts container versions not yet present in the status chain. - /// - /// When initially creating a new versioned item, the code doesn't have - /// access to the versions defined on the container. This function inserts - /// all non-present container versions and decides which status and ident - /// is the right fit based on the status neighbors. - /// - /// This continuous chain ensures that when generating code (tokens), each - /// field can lookup the status (and ident) for a requested version. - fn insert_container_versions(&mut self, versions: &[VersionDefinition]); - - /// Returns the ident of the item based on the provided container version. - fn get_ident(&self, version: &VersionDefinition) -> Option<&Ident>; -} - -pub(crate) trait InnerItem: Named + Spanned { - fn ty(&self) -> Type; -} - -/// This trait enables access to the ident of named items, like fields and -/// variants. -/// -/// It additionally provides a function to retrieve the cleaned ident, which -/// removes the deprecation prefixes. -pub(crate) trait Named { - fn cleaned_ident(&self) -> Ident; - fn ident(&self) -> &Ident; -} - -/// This trait enables access to the common and original attributes across field -/// and variant attributes. -pub(crate) trait Attributes { - /// The common attributes defined by the versioned macro. - fn common_attributes_owned(self) -> ItemAttributes; - - /// The common attributes defined by the versioned macro. - fn common_attributes(&self) -> &ItemAttributes; - - /// The attributes applied to the item outside of the versioned macro. - fn original_attributes(&self) -> &Vec; -} - -/// This struct combines common code for versioned fields and variants. -/// -/// Most of the initial creation of a versioned field and variant are identical. -/// Currently, the following steps are unified: -/// -/// - Initial creation of the action chain based on item attributes. -/// - Insertion of container versions into the chain. -/// -/// The generic type parameter `I` describes the type of the versioned item, -/// usually [`Field`](syn::Field) or [`Variant`](syn::Variant). The parameter -/// `A` indicates the type of item attributes, usually [`FieldAttributes`][1] or -/// [`VariantAttributes`][2] depending on the used item type. As this type is -/// only needed during creation of [`Self`](VersionedItem), we must use a -/// [`PhantomData`] marker. -/// -/// [1]: crate::attrs::field::FieldAttributes -/// [2]: crate::attrs::variant::VariantAttributes -#[derive(Debug)] -pub(crate) struct VersionedItem -where - A: for<'i> TryFrom<&'i I> + Attributes, - I: InnerItem, -{ - pub(crate) original_attributes: Vec, - pub(crate) chain: Option, - pub(crate) inner: I, - _marker: PhantomData, -} - -impl Item for VersionedItem -where - syn::Error: for<'i> From<>::Error>, - A: for<'i> TryFrom<&'i I> + Attributes + ValidateVersions, - I: InnerItem, -{ - fn new(item: I, container_attrs: &StandaloneContainerAttributes) -> syn::Result { - // We use the TryFrom trait here, because the type parameter `A` can use - // it as a trait bound. Internally this then calls either `from_field` - // for field attributes or `from_variant` for variant attributes. Sadly - // darling doesn't provide a "generic" trait which abstracts over the - // different `from_` functions. - let attrs = A::try_from(&item)?; - attrs.validate_versions(container_attrs, &item)?; - - // These are the attributes added to the item outside of the macro. - let original_attributes = attrs.original_attributes().clone(); - - // These are the versioned macro attrs that are common to all items. - let common_attributes = attrs.common_attributes_owned(); - - // Constructing the action chain requires going through the actions - // starting at the end, because the container definition always - // represents the latest (most up-to-date) version of that struct. - // That's why the following code needs to go through the actions in - // reverse order, as otherwise it is impossible to extract the item - // ident for each version. - - // Deprecating an item is always the last state an item can end up in. - // For items which are not deprecated, the last change is either the - // latest change or addition, which is handled below. The ident of the - // deprecated item is guaranteed to include the 'deprecated_' or - // 'DEPRECATED_' prefix. The ident can thus be used as is. - if let Some(deprecated) = common_attributes.deprecated { - let deprecated_ident = item.ident(); - - // When the item is deprecated, any change which occurred beforehand - // requires access to the item ident to infer the item ident for - // the latest change. - let mut ident = item.cleaned_ident(); - let mut ty = item.ty(); - - let mut actions = BTreeMap::new(); - - actions.insert( - *deprecated.since, - ItemStatus::Deprecation { - previous_ident: ident.clone(), - ident: deprecated_ident.clone(), - note: deprecated.note.as_deref().cloned(), - }, - ); - - for change in common_attributes.changes.iter().rev() { - let from_ident = if let Some(from) = change.from_name.as_deref() { - format_ident!("{from}") - } else { - ident.clone() - }; - - // TODO (@Techassi): This is an awful lot of cloning, can we get - // rid of it? - let from_ty = change - .from_type - .as_ref() - .map(|sv| sv.deref().clone()) - .unwrap_or(ty.clone()); - - actions.insert( - *change.since, - ItemStatus::Change { - from_ident: from_ident.clone(), - to_ident: ident, - from_type: from_ty.clone(), - to_type: ty, - }, - ); - - ident = from_ident; - ty = from_ty; - } - - // After the last iteration above (if any) we use the ident for the - // added action if there is any. - if let Some(added) = common_attributes.added { - actions.insert( - *added.since, - ItemStatus::Addition { - default_fn: added.default_fn.deref().clone(), - ident, - ty, - }, - ); - } - - Ok(Self { - _marker: PhantomData, - chain: Some(actions), - original_attributes, - inner: item, - }) - } else if !common_attributes.changes.is_empty() { - let mut ident = item.ident().clone(); - let mut ty = item.ty(); - - let mut actions = BTreeMap::new(); - - for change in common_attributes.changes.iter().rev() { - let from_ident = if let Some(from) = change.from_name.as_deref() { - format_ident!("{from}") - } else { - ident.clone() - }; - - // TODO (@Techassi): This is an awful lot of cloning, can we get - // rid of it? - let from_ty = change - .from_type - .as_ref() - .map(|sv| sv.deref().clone()) - .unwrap_or(ty.clone()); - - actions.insert( - *change.since, - ItemStatus::Change { - from_ident: from_ident.clone(), - to_ident: ident, - from_type: from_ty.clone(), - to_type: ty, - }, - ); - - ident = from_ident; - ty = from_ty; - } - - // After the last iteration above (if any) we use the ident for the - // added action if there is any. - if let Some(added) = common_attributes.added { - actions.insert( - *added.since, - ItemStatus::Addition { - default_fn: added.default_fn.deref().clone(), - ident, - ty, - }, - ); - } - - Ok(Self { - _marker: PhantomData, - chain: Some(actions), - original_attributes, - inner: item, - }) - } else { - if let Some(added) = common_attributes.added { - let mut actions = BTreeMap::new(); - - actions.insert( - *added.since, - ItemStatus::Addition { - default_fn: added.default_fn.deref().clone(), - ident: item.ident().clone(), - ty: item.ty(), - }, - ); - - return Ok(Self { - _marker: PhantomData, - chain: Some(actions), - original_attributes, - inner: item, - }); - } - - Ok(Self { - _marker: PhantomData, - original_attributes, - chain: None, - inner: item, - }) - } - } - - fn insert_container_versions(&mut self, versions: &[VersionDefinition]) { - if let Some(chain) = &mut self.chain { - for version in versions { - if chain.contains_key(&version.inner) { - continue; - } - - match chain.get_neighbors(&version.inner) { - (None, Some(status)) => match status { - ItemStatus::Addition { .. } => { - chain.insert(version.inner, ItemStatus::NotPresent) - } - ItemStatus::Change { - from_ident, - from_type, - .. - } => chain.insert( - version.inner, - ItemStatus::NoChange { - previously_deprecated: false, - ident: from_ident.clone(), - ty: from_type.clone(), - }, - ), - ItemStatus::Deprecation { previous_ident, .. } => chain.insert( - version.inner, - ItemStatus::NoChange { - previously_deprecated: false, - ident: previous_ident.clone(), - ty: self.inner.ty(), - }, - ), - ItemStatus::NoChange { - previously_deprecated, - ident, - ty, - } => chain.insert( - version.inner, - ItemStatus::NoChange { - previously_deprecated: *previously_deprecated, - ident: ident.clone(), - ty: ty.clone(), - }, - ), - ItemStatus::NotPresent => unreachable!(), - }, - (Some(status), None) => { - let (ident, ty, previously_deprecated) = match status { - ItemStatus::Addition { ident, ty, .. } => (ident, ty, false), - ItemStatus::Change { - to_ident, to_type, .. - } => (to_ident, to_type, false), - ItemStatus::Deprecation { ident, .. } => { - (ident, &self.inner.ty(), true) - } - ItemStatus::NoChange { - previously_deprecated, - ident, - ty, - .. - } => (ident, ty, *previously_deprecated), - ItemStatus::NotPresent => unreachable!(), - }; - - chain.insert( - version.inner, - ItemStatus::NoChange { - previously_deprecated, - ident: ident.clone(), - ty: ty.clone(), - }, - ) - } - (Some(status), Some(_)) => { - let (ident, ty, previously_deprecated) = match status { - ItemStatus::Addition { ident, ty, .. } => (ident, ty, false), - ItemStatus::Change { - to_ident, to_type, .. - } => (to_ident, to_type, false), - ItemStatus::NoChange { - previously_deprecated, - ident, - ty, - .. - } => (ident, ty, *previously_deprecated), - _ => unreachable!(), - }; - - chain.insert( - version.inner, - ItemStatus::NoChange { - previously_deprecated, - ident: ident.clone(), - ty: ty.clone(), - }, - ) - } - _ => unreachable!(), - }; - } - } - } - - fn get_ident(&self, version: &VersionDefinition) -> Option<&Ident> { - match &self.chain { - Some(chain) => chain - .get(&version.inner) - .expect("internal error: chain must contain container version") - .get_ident(), - None => Some(self.inner.ident()), - } - } -} - -#[derive(Debug, PartialEq)] -pub(crate) enum ItemStatus { - Addition { - ident: Ident, - default_fn: Path, - // NOTE (@Techassi): We need to carry idents and type information in - // nearly every status. Ideally, we would store this in separate maps. - ty: Type, - }, - Change { - from_ident: Ident, - to_ident: Ident, - from_type: Type, - to_type: Type, - }, - Deprecation { - previous_ident: Ident, - note: Option, - ident: Ident, - }, - NoChange { - previously_deprecated: bool, - ident: Ident, - ty: Type, - }, - NotPresent, -} - -impl ItemStatus { - pub(crate) fn get_ident(&self) -> Option<&Ident> { - match &self { - ItemStatus::Addition { ident, .. } => Some(ident), - ItemStatus::Change { to_ident, .. } => Some(to_ident), - ItemStatus::Deprecation { ident, .. } => Some(ident), - ItemStatus::NoChange { ident, .. } => Some(ident), - ItemStatus::NotPresent => None, - } - } -} diff --git a/crates/stackable-versioned-macros/src/codegen/common/mod.rs b/crates/stackable-versioned-macros/src/codegen/common/mod.rs deleted file mode 100644 index ecbc09e8a..000000000 --- a/crates/stackable-versioned-macros/src/codegen/common/mod.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::collections::BTreeMap; - -use k8s_version::Version; -use proc_macro2::Span; -use quote::format_ident; -use syn::Ident; - -use crate::{ - attrs::common::{ModuleAttributes, StandaloneContainerAttributes}, - consts::{DEPRECATED_FIELD_PREFIX, DEPRECATED_VARIANT_PREFIX}, -}; - -mod container; -mod item; -mod module; - -pub(crate) use container::*; -pub(crate) use item::*; -pub(crate) use module::*; - -/// Type alias to make the type of the version chain easier to handle. -pub(crate) type VersionChain = BTreeMap; - -#[derive(Debug, Clone)] -pub(crate) struct VersionDefinition { - /// Indicates that the container version is deprecated. - pub(crate) deprecated: bool, - - /// Indicates that the generation of `From for NEW` should be skipped. - pub(crate) skip_from: bool, - - /// A validated Kubernetes API version. - pub(crate) inner: Version, - - /// The ident of the container. - pub(crate) ident: Ident, - - /// Store additional doc-comment lines for this version. - pub(crate) version_specific_docs: Vec, -} - -/// Converts lines of doc-comments into a trimmed list. -fn process_docs(input: &Option) -> Vec { - if let Some(input) = input { - input - // Trim the leading and trailing whitespace, deleting suprefluous - // empty lines. - .trim() - .lines() - // Trim the leading and trailing whitespace on each line that can be - // introduced when the developer indents multi-line comments. - .map(|line| line.trim().to_owned()) - .collect() - } else { - Vec::new() - } -} - -// NOTE (@Techassi): Can we maybe unify these two impls? -impl From<&StandaloneContainerAttributes> for Vec { - fn from(attributes: &StandaloneContainerAttributes) -> Self { - attributes - .versions - .iter() - .map(|v| VersionDefinition { - skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()), - ident: Ident::new(&v.name.to_string(), Span::call_site()), - version_specific_docs: process_docs(&v.doc), - deprecated: v.deprecated.is_present(), - inner: v.name, - }) - .collect() - } -} - -impl From<&ModuleAttributes> for Vec { - fn from(attributes: &ModuleAttributes) -> Self { - attributes - .versions - .iter() - .map(|v| VersionDefinition { - skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()), - ident: format_ident!("{version}", version = v.name.to_string()), - version_specific_docs: process_docs(&v.doc), - deprecated: v.deprecated.is_present(), - inner: v.name, - }) - .collect() - } -} - -/// Removes the deprecated prefix from a field ident. -/// -/// See [`DEPRECATED_FIELD_PREFIX`]. -pub(crate) fn remove_deprecated_field_prefix(ident: &Ident) -> Ident { - let ident = ident.to_string(); - let ident = ident.trim_start_matches(DEPRECATED_FIELD_PREFIX); - - format_ident!("{ident}") -} - -/// Removes the deprecated prefix from a variant ident. -/// -/// See [`DEPRECATED_VARIANT_PREFIX`]. -pub(crate) fn remove_deprecated_variant_prefix(ident: &Ident) -> Ident { - // NOTE (@Techassi): Currently Clippy only issues a warning for variants - // with underscores in their name. That's why we additionally remove the - // leading underscore from the ident to use the expected name during code - // generation. - let ident = ident.to_string(); - let ident = ident - .trim_start_matches(DEPRECATED_VARIANT_PREFIX) - .trim_start_matches('_'); - - format_ident!("{ident}") -} diff --git a/crates/stackable-versioned-macros/src/codegen/common/module.rs b/crates/stackable-versioned-macros/src/codegen/common/module.rs deleted file mode 100644 index 00c047aea..000000000 --- a/crates/stackable-versioned-macros/src/codegen/common/module.rs +++ /dev/null @@ -1,31 +0,0 @@ -use proc_macro2::TokenStream; -use quote::quote; -use syn::Visibility; - -use crate::codegen::common::VersionDefinition; - -pub(crate) fn generate_module( - version: &VersionDefinition, - visibility: &Visibility, - content: TokenStream, -) -> TokenStream { - let version_ident = &version.ident; - - let deprecated_attribute = version.deprecated.then(|| { - let deprecated_note = format!("Version {version_ident} is deprecated"); - - quote! { - #[deprecated = #deprecated_note] - } - }); - - quote! { - #[automatically_derived] - #deprecated_attribute - #visibility mod #version_ident { - use super::*; - - #content - } - } -} diff --git a/crates/stackable-versioned-macros/src/codegen/container/enum.rs b/crates/stackable-versioned-macros/src/codegen/container/enum.rs new file mode 100644 index 000000000..ff251695d --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/container/enum.rs @@ -0,0 +1,191 @@ +use std::ops::Not; + +use darling::{FromAttributes, Result}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::ItemEnum; + +use crate::{ + attrs::container::NestedContainerAttributes, + codegen::{ + changes::Neighbors, + container::{CommonContainerData, Container, ContainerIdents, ContainerOptions}, + item::VersionedVariant, + ItemStatus, StandaloneContainerAttributes, VersionDefinition, + }, +}; + +impl Container { + pub(crate) fn new_standalone_enum( + item_enum: ItemEnum, + attributes: StandaloneContainerAttributes, + versions: &[VersionDefinition], + ) -> Result { + let mut versioned_variants = Vec::new(); + for variant in item_enum.variants { + let mut versioned_variant = VersionedVariant::new(variant, versions)?; + versioned_variant.insert_container_versions(versions); + versioned_variants.push(versioned_variant); + } + + let options = ContainerOptions { + kubernetes_options: None, + skip_from: attributes + .common_root_arguments + .options + .skip + .map_or(false, |s| s.from.is_present()), + }; + + let idents: ContainerIdents = item_enum.ident.into(); + + let common = CommonContainerData { + original_attributes: item_enum.attrs, + options, + idents, + }; + + Ok(Self::Enum(Enum { + variants: versioned_variants, + common, + })) + } + + // TODO (@Techassi): See what can be unified into a single 'new' function + pub(crate) fn new_enum_nested( + item_enum: ItemEnum, + versions: &[VersionDefinition], + ) -> Result { + let attributes = NestedContainerAttributes::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)?; + versioned_variant.insert_container_versions(versions); + versioned_variants.push(versioned_variant); + } + + let options = ContainerOptions { + kubernetes_options: None, + skip_from: attributes + .options + .skip + .map_or(false, |s| s.from.is_present()), + }; + + let idents: ContainerIdents = item_enum.ident.into(); + + let common = CommonContainerData { + original_attributes: item_enum.attrs, + options, + idents, + }; + + Ok(Self::Enum(Enum { + variants: versioned_variants, + common, + })) + } +} + +pub(crate) struct Enum { + /// List of variants defined in the original enum. How, and if, an item + /// should generate code, is decided by the currently generated version. + pub(crate) variants: Vec, + pub(crate) common: CommonContainerData, +} + +impl Enum { + pub(crate) fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { + let original_attributes = &self.common.original_attributes; + let ident = &self.common.idents.original; + + let mut variants = TokenStream::new(); + for variant in &self.variants { + variants.extend(variant.generate_for_container(version)); + } + + quote! { + #(#original_attributes)* + pub enum #ident { + #variants + } + } + } + + pub(crate) fn generate_from_impl( + &self, + version: &VersionDefinition, + next_version: Option<&VersionDefinition>, + is_nested: bool, + ) -> Option { + match next_version { + Some(next_version) => { + let enum_ident = &self.common.idents.original; + let from_ident = &self.common.idents.from; + + let next_version_ident = &next_version.ident; + let version_ident = &version.ident; + + let mut variants = TokenStream::new(); + for variant in &self.variants { + variants.extend(variant.generate_for_from_impl( + version, + next_version, + enum_ident, + )); + } + + // 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 + || next_version.deprecated + || 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 = + is_nested.not().then(|| quote! {#[automatically_derived]}); + + Some(quote! { + #automatically_derived + #allow_attribute + impl ::std::convert::From<#version_ident::#enum_ident> for #next_version_ident::#enum_ident { + fn from(#from_ident: #version_ident::#enum_ident) -> Self { + match #from_ident { + #variants + } + } + } + }) + } + None => None, + } + } + + /// Returns whether any variant is deprecated in the provided `version`. + fn is_any_variant_deprecated(&self, version: &VersionDefinition) -> bool { + // First, iterate over all variants. Any will return true if any of the + // function invocations return true. If a field doesn't have a chain, + // we can safely default to false (unversioned fields cannot be + // deprecated). Then we retrieve the status of the field and ensure it + // is deprecated. + self.variants.iter().any(|f| { + f.changes.as_ref().map_or(false, |c| { + c.value_is(&version.inner, |a| { + matches!( + a, + ItemStatus::Deprecation { .. } + | ItemStatus::NoChange { + previously_deprecated: true, + .. + } + ) + }) + }) + }) + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/container/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/mod.rs new file mode 100644 index 000000000..7907544f4 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/container/mod.rs @@ -0,0 +1,218 @@ +use darling::{util::IdentString, Result}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Attribute, Ident, ItemEnum, ItemStruct, Visibility}; + +use crate::{ + attrs::{container::StandaloneContainerAttributes, k8s::KubernetesArguments}, + codegen::{ + container::{r#enum::Enum, r#struct::Struct}, + VersionDefinition, + }, + utils::ContainerIdentExt, +}; + +mod r#enum; +mod r#struct; + +pub(crate) struct CommonContainerData { + /// Original attributes placed on the container, like `#[derive()]` or `#[cfg()]`. + pub(crate) original_attributes: Vec, + + /// Different options which influence code generation. + pub(crate) options: ContainerOptions, + + /// A collection of container idents used for different purposes. + pub(crate) idents: ContainerIdents, +} + +pub(crate) enum Container { + Struct(Struct), + Enum(Enum), +} + +impl Container { + pub(crate) fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { + match self { + Container::Struct(s) => s.generate_definition(version), + Container::Enum(e) => e.generate_definition(version), + } + } + + pub(crate) fn generate_from_impl( + &self, + version: &VersionDefinition, + next_version: Option<&VersionDefinition>, + is_nested: bool, + ) -> Option { + match self { + Container::Struct(s) => s.generate_from_impl(version, next_version, is_nested), + Container::Enum(e) => e.generate_from_impl(version, next_version, is_nested), + } + } + + pub(crate) fn generate_kubernetes_item( + &self, + version: &VersionDefinition, + ) -> Option<(IdentString, TokenStream)> { + match self { + Container::Struct(s) => s.generate_kubernetes_item(version), + Container::Enum(_) => None, + } + } + + pub(crate) fn generate_kubernetes_merge_crds( + &self, + enum_variants: Vec, + fn_calls: Vec, + is_nested: bool, + ) -> Option { + match self { + Container::Struct(s) => { + s.generate_kubernetes_merge_crds(enum_variants, fn_calls, is_nested) + } + Container::Enum(_) => None, + } + } +} + +pub(crate) struct StandaloneContainer { + versions: Vec, + container: Container, + vis: Visibility, +} + +impl StandaloneContainer { + pub(crate) fn new_struct( + item_struct: ItemStruct, + attributes: StandaloneContainerAttributes, + ) -> Result { + // TODO (@Techassi): Only pass the fields we need from item struct instead of moving as a whole + let versions: Vec<_> = (&attributes).into(); + let vis = item_struct.vis.clone(); + + let container = Container::new_standalone_struct(item_struct, attributes, &versions)?; + + Ok(Self { + container, + versions, + vis, + }) + } + + pub(crate) fn new_enum( + item_enum: ItemEnum, + attributes: StandaloneContainerAttributes, + ) -> Result { + let versions: Vec<_> = (&attributes).into(); + let vis = item_enum.vis.clone(); + + let container = Container::new_standalone_enum(item_enum, attributes, &versions)?; + + Ok(Self { + container, + versions, + vis, + }) + } + + pub(crate) fn generate_tokens(&self) -> TokenStream { + let vis = &self.vis; + + let mut tokens = TokenStream::new(); + + let mut kubernetes_merge_crds_fn_calls = Vec::new(); + let mut kubernetes_enum_variants = Vec::new(); + + let mut versions = self.versions.iter().peekable(); + + while let Some(version) = versions.next() { + let container_definition = self.container.generate_definition(version); + + // NOTE (@Techassi): Using '.copied()' here does not copy or clone the data, but instead + // removes one level of indirection of the double reference '&&'. + let from_impl = + self.container + .generate_from_impl(version, versions.peek().copied(), false); + + // Generate Kubernetes specific code which is placed outside of the container + // definition. + if let Some((enum_variant, fn_call)) = self.container.generate_kubernetes_item(version) + { + kubernetes_merge_crds_fn_calls.push(fn_call); + kubernetes_enum_variants.push(enum_variant); + } + + let version_ident = &version.ident; + + tokens.extend(quote! { + #vis mod #version_ident { + #container_definition + } + + #from_impl + }); + } + + tokens.extend(self.container.generate_kubernetes_merge_crds( + kubernetes_enum_variants, + kubernetes_merge_crds_fn_calls, + false, + )); + + tokens + } +} + +/// A collection of container idents used for different purposes. +#[derive(Debug)] +pub(crate) struct ContainerIdents { + /// The ident used in the context of Kubernetes specific code. This ident + /// removes the 'Spec' suffix present in the definition container. + pub(crate) kubernetes: IdentString, + + /// The original ident, or name, of the versioned container. + pub(crate) original: IdentString, + + /// The ident used in the [`From`] impl. + pub(crate) from: IdentString, +} + +impl From for ContainerIdents { + fn from(ident: Ident) -> Self { + Self { + kubernetes: ident.as_cleaned_kubernetes_ident(), + from: ident.as_from_impl_ident(), + original: ident.into(), + } + } +} + +#[derive(Debug)] +pub(crate) struct ContainerOptions { + pub(crate) kubernetes_options: Option, + pub(crate) skip_from: bool, +} + +#[derive(Debug)] +pub(crate) struct KubernetesOptions { + pub(crate) singular: Option, + pub(crate) plural: Option, + pub(crate) skip_merged_crd: bool, + pub(crate) kind: Option, + pub(crate) namespaced: bool, + pub(crate) group: String, +} + +impl From for KubernetesOptions { + fn from(args: KubernetesArguments) -> Self { + KubernetesOptions { + skip_merged_crd: args.skip.map_or(false, |s| s.merged_crd.is_present()), + namespaced: args.namespaced.is_present(), + singular: args.singular, + plural: args.plural, + group: args.group, + kind: args.kind, + } + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct.rs b/crates/stackable-versioned-macros/src/codegen/container/struct.rs new file mode 100644 index 000000000..1e83c58d7 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/container/struct.rs @@ -0,0 +1,330 @@ +use std::ops::Not; + +use darling::{util::IdentString, FromAttributes, Result}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{parse_quote, ItemStruct, Path}; + +use crate::{ + attrs::container::NestedContainerAttributes, + codegen::{ + changes::Neighbors, + container::{CommonContainerData, Container, ContainerIdents, ContainerOptions}, + item::VersionedField, + ItemStatus, StandaloneContainerAttributes, VersionDefinition, + }, + utils::VersionExt, +}; + +impl Container { + pub(crate) fn new_standalone_struct( + item_struct: ItemStruct, + attributes: StandaloneContainerAttributes, + versions: &[VersionDefinition], + ) -> Result { + // NOTE (@Techassi): Should we check if the fields are named here? + let mut versioned_fields = Vec::new(); + + for field in item_struct.fields { + let mut versioned_field = VersionedField::new(field, versions)?; + versioned_field.insert_container_versions(versions); + versioned_fields.push(versioned_field); + } + + let kubernetes_options = attributes.kubernetes_arguments.map(Into::into); + + let options = ContainerOptions { + skip_from: attributes + .common_root_arguments + .options + .skip + .map_or(false, |s| s.from.is_present()), + kubernetes_options, + }; + + let idents: ContainerIdents = item_struct.ident.into(); + + let common = CommonContainerData { + original_attributes: item_struct.attrs, + options, + idents, + }; + + Ok(Self::Struct(Struct { + fields: versioned_fields, + common, + })) + } + + // TODO (@Techassi): See what can be unified into a single 'new' function + pub(crate) fn new_struct_nested( + item_struct: ItemStruct, + versions: &[VersionDefinition], + ) -> Result { + let attributes = NestedContainerAttributes::from_attributes(&item_struct.attrs)?; + + let mut versioned_fields = Vec::new(); + for field in item_struct.fields { + let mut versioned_field = VersionedField::new(field, versions)?; + versioned_field.insert_container_versions(versions); + versioned_fields.push(versioned_field); + } + + let kubernetes_options = attributes.kubernetes_arguments.map(Into::into); + + let options = ContainerOptions { + skip_from: attributes + .options + .skip + .map_or(false, |s| s.from.is_present()), + kubernetes_options, + }; + + let idents: ContainerIdents = item_struct.ident.into(); + + // Nested structs + // We need to filter out the `versioned` attribute, because these are not directly processed + // by darling, but instead by us (using darling). For this reason, darling won't remove the + // attribute from the input and as such, we need to filter it out ourself. + let original_attributes = item_struct + .attrs + .into_iter() + .filter(|attr| !attr.meta.path().is_ident("versioned")) + .collect(); + + let common = CommonContainerData { + original_attributes, + options, + idents, + }; + + Ok(Self::Struct(Struct { + fields: versioned_fields, + common, + })) + } + + fn new_struct() -> Result { + todo!() + } +} + +pub(crate) struct Struct { + /// List of fields defined in the original struct. How, and if, an item + /// should generate code, is decided by the currently generated version. + pub(crate) fields: Vec, + pub(crate) common: CommonContainerData, +} + +// Common token generation +impl Struct { + pub(crate) fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { + let original_attributes = &self.common.original_attributes; + let ident = &self.common.idents.original; + + let mut fields = TokenStream::new(); + for field in &self.fields { + fields.extend(field.generate_for_container(version)); + } + + // This only returns Some, if K8s features are enabled + let kubernetes_cr_derive = self.generate_kubernetes_cr_derive(version); + + quote! { + #(#original_attributes)* + #kubernetes_cr_derive + pub struct #ident { + #fields + } + } + } + + pub(crate) fn generate_from_impl( + &self, + version: &VersionDefinition, + next_version: Option<&VersionDefinition>, + is_nested: bool, + ) -> Option { + if version.skip_from || self.common.options.skip_from { + return None; + } + + match next_version { + Some(next_version) => { + let struct_ident = &self.common.idents.original; + let from_struct_ident = &self.common.idents.from; + + let for_module_ident = &next_version.ident; + let from_module_ident = &version.ident; + + let fields = self.generate_from_fields(version, next_version, from_struct_ident); + + // Include allow(deprecated) only when this or the next version is + // deprecated. Also include it, when a field in this or the next + // version is deprecated. + let allow_attribute = (version.deprecated + || next_version.deprecated + || self.is_any_field_deprecated(version) + || self.is_any_field_deprecated(next_version)) + .then(|| 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 = + is_nested.not().then(|| quote! {#[automatically_derived]}); + + Some(quote! { + #automatically_derived + #allow_attribute + impl ::std::convert::From<#from_module_ident::#struct_ident> for #for_module_ident::#struct_ident { + fn from(#from_struct_ident: #from_module_ident::#struct_ident) -> Self { + Self { + #fields + } + } + } + }) + } + None => None, + } + } + + fn generate_from_fields( + &self, + version: &VersionDefinition, + next_version: &VersionDefinition, + from_struct_ident: &IdentString, + ) -> TokenStream { + let mut tokens = TokenStream::new(); + + for field in &self.fields { + tokens.extend(field.generate_for_from_impl(version, next_version, from_struct_ident)); + } + + tokens + } + + fn is_any_field_deprecated(&self, version: &VersionDefinition) -> bool { + self.fields.iter().any(|f| { + f.changes.as_ref().map_or(false, |c| { + c.value_is(&version.inner, |a| { + matches!( + a, + ItemStatus::Deprecation { .. } + | ItemStatus::NoChange { + previously_deprecated: true, + .. + } + ) + }) + }) + }) + } +} + +// Kubernetes-specific token generation +impl Struct { + pub(crate) fn generate_kubernetes_cr_derive( + &self, + version: &VersionDefinition, + ) -> Option { + match &self.common.options.kubernetes_options { + Some(kubernetes_options) => { + // Required arguments + let group = &kubernetes_options.group; + let version = version.inner.to_string(); + let kind = kubernetes_options + .kind + .as_ref() + .map_or(self.common.idents.kubernetes.to_string(), |kind| { + kind.clone() + }); + + // Optional arguments + let namespaced = kubernetes_options + .namespaced + .then_some(quote! { , namespaced }); + let singular = kubernetes_options + .singular + .as_ref() + .map(|s| quote! { , singular = #s }); + let plural = kubernetes_options + .plural + .as_ref() + .map(|p| quote! { , plural = #p }); + + Some(quote! { + #[derive(::kube::CustomResource)] + #[kube(group = #group, version = #version, kind = #kind #singular #plural #namespaced)] + }) + } + None => None, + } + } + + pub(crate) fn generate_kubernetes_item( + &self, + version: &VersionDefinition, + ) -> Option<(IdentString, TokenStream)> { + match &self.common.options.kubernetes_options { + Some(_) => { + let enum_variant_ident = version.inner.as_variant_ident(); + + let struct_ident = &self.common.idents.kubernetes; + let module_ident = &version.ident; + let qualified_path: Path = parse_quote!(#module_ident::#struct_ident); + + let merge_crds_fn_call = quote! { + <#qualified_path as ::kube::CustomResourceExt>::crd() + }; + + Some((enum_variant_ident, merge_crds_fn_call)) + } + None => None, + } + } + + pub(crate) fn generate_kubernetes_merge_crds( + &self, + enum_variants: Vec, + fn_calls: Vec, + is_nested: bool, + ) -> Option { + if enum_variants.is_empty() { + return None; + } + + let enum_ident = &self.common.idents.kubernetes; + + // 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]}); + + // TODO (@Techassi): Use proper visibility instead of hard-coding 'pub' + Some(quote! { + #automatically_derived + pub enum #enum_ident { + #(#enum_variants),* + } + + #automatically_derived + impl ::std::fmt::Display for #enum_ident { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { + match self { + #(Self::#enum_variants => f.write_str("stuff")),* + } + } + } + + #automatically_derived + impl #enum_ident { + /// Generates a merged CRD which contains all versions defined using the `#[versioned()]` macro. + pub fn merged_crd( + stored_apiversion: Self + ) -> ::std::result::Result<::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError> { + ::kube::core::crd::merge_crds(vec![#(#fn_calls),*], &stored_apiversion.to_string()) + } + } + }) + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/vstruct/field.rs b/crates/stackable-versioned-macros/src/codegen/item/field.rs similarity index 56% rename from crates/stackable-versioned-macros/src/codegen/vstruct/field.rs rename to crates/stackable-versioned-macros/src/codegen/item/field.rs index 637c766bc..951ce774b 100644 --- a/crates/stackable-versioned-macros/src/codegen/vstruct/field.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/field.rs @@ -1,109 +1,60 @@ -use std::ops::{Deref, DerefMut}; +use std::collections::BTreeMap; -use darling::{util::IdentString, FromField}; +use darling::{util::IdentString, FromField, Result}; +use k8s_version::Version; use proc_macro2::TokenStream; use quote::quote; -use syn::{Field, Ident}; +use syn::{Attribute, Field, Type}; use crate::{ - attrs::{ - common::{ItemAttributes, StandaloneContainerAttributes}, - field::FieldAttributes, - }, - codegen::common::{ - remove_deprecated_field_prefix, Attributes, InnerItem, Item, ItemStatus, Named, - VersionDefinition, VersionedItem, + attrs::item::FieldAttributes, + codegen::{ + changes::{BTreeMapExt, ChangesetExt}, + ItemStatus, VersionDefinition, }, + utils::FieldIdent, }; -/// A versioned field, which contains common [`Field`] data and a chain of -/// actions. -/// -/// The chain of actions maps versions to an action and the appropriate field -/// name. -/// -/// Additionally, the [`Field`] data can be used to forward attributes, generate -/// documentation, etc. -#[derive(Debug)] -pub(crate) struct VersionedField(VersionedItem); - -impl Deref for VersionedField { - type Target = VersionedItem; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for VersionedField { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl TryFrom<&Field> for FieldAttributes { - type Error = darling::Error; - - fn try_from(field: &Field) -> Result { - Self::from_field(field) - } +pub(crate) struct VersionedField { + pub(crate) original_attributes: Vec, + pub(crate) changes: Option>, + pub(crate) ident: FieldIdent, + pub(crate) ty: Type, } -impl Attributes for FieldAttributes { - fn common_attributes_owned(self) -> ItemAttributes { - self.common - } - - fn common_attributes(&self) -> &ItemAttributes { - &self.common - } - - fn original_attributes(&self) -> &Vec { - &self.attrs - } -} - -impl InnerItem for Field { - fn ty(&self) -> syn::Type { - self.ty.clone() - } -} - -impl Named for Field { - fn cleaned_ident(&self) -> Ident { - let ident = self.ident(); - remove_deprecated_field_prefix(ident) +impl VersionedField { + pub(crate) fn new(field: Field, versions: &[VersionDefinition]) -> Result { + // TODO (@Techassi): Remove unwrap + let field_attributes = FieldAttributes::from_field(&field)?; + field_attributes.validate_versions(versions)?; + + let field_ident = FieldIdent::from(field.ident.unwrap()); + let changes = field_attributes + .common + .into_changeset(&field_ident, field.ty.clone()); + + Ok(Self { + original_attributes: field_attributes.attrs, + ident: field_ident, + ty: field.ty, + changes, + }) } - fn ident(&self) -> &Ident { - self.ident - .as_ref() - .expect("internal error: field must have an ident") - } -} - -impl VersionedField { - /// Creates a new versioned field. - /// - /// Internally this calls [`VersionedItem::new`] to handle most of the - /// common creation code. - pub(crate) fn new( - field: Field, - container_attributes: &StandaloneContainerAttributes, - ) -> syn::Result { - let item = VersionedItem::<_, FieldAttributes>::new(field, container_attributes)?; - Ok(Self(item)) + pub(crate) fn insert_container_versions(&mut self, versions: &[VersionDefinition]) { + if let Some(changes) = &mut self.changes { + changes.insert_container_versions(versions, &self.ty); + } } - /// Generates tokens to be used in a container definition. pub(crate) fn generate_for_container( &self, - container_version: &VersionDefinition, + version: &VersionDefinition, ) -> Option { let original_attributes = &self.original_attributes; - match &self.chain { - Some(chain) => { + 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 @@ -112,13 +63,13 @@ impl VersionedField { // The code generation then depends on the relation to other // versions (with actions). - let field_type = &self.inner.ty; + let field_type = &self.ty; // NOTE (@Techassi): https://rust-lang.github.io/rust-clippy/master/index.html#/expect_fun_call - match chain.get(&container_version.inner).unwrap_or_else(|| { + match changes.get(&version.inner).unwrap_or_else(|| { panic!( "internal error: chain must contain container version {}", - container_version.inner + version.inner ) }) { ItemStatus::Addition { ident, ty, .. } => Some(quote! { @@ -178,8 +129,8 @@ impl VersionedField { None => { // If there is no chain of field actions, the field is not // versioned and therefore included in all versions. - let field_ident = &self.inner.ident; - let field_type = &self.inner.ty; + let field_ident = &self.ident; + let field_type = &self.ty; Some(quote! { #(#original_attributes)* @@ -189,23 +140,18 @@ impl VersionedField { } } - /// Generates tokens to be used in a [`From`] implementation. pub(crate) fn generate_for_from_impl( &self, version: &VersionDefinition, next_version: &VersionDefinition, - from_ident: &IdentString, + from_struct_ident: &IdentString, ) -> TokenStream { - match &self.chain { - Some(chain) => { - match ( - chain - .get(&version.inner) - .expect("internal error: chain must contain container version"), - chain - .get(&next_version.inner) - .expect("internal error: chain must contain container version"), - ) { + match &self.changes { + Some(changes) => { + let next_change = changes.get_expect(&next_version.inner); + let change = changes.get_expect(&version.inner); + + match (change, next_change) { ( _, ItemStatus::Addition { @@ -225,33 +171,29 @@ impl VersionedField { ) => { if from_type == to_type { quote! { - #to_ident: #from_ident.#old_field_ident, + #to_ident: #from_struct_ident.#old_field_ident, } } else { quote! { - #to_ident: #from_ident.#old_field_ident.into(), + #to_ident: #from_struct_ident.#old_field_ident.into(), } } } (old, next) => { - let old_field_ident = old - .get_ident() - .expect("internal error: old field must have a name"); - - let next_field_ident = next - .get_ident() - .expect("internal error: new field must have a name"); + let next_field_ident = next.get_ident(); + let old_field_ident = old.get_ident(); quote! { - #next_field_ident: #from_ident.#old_field_ident, + #next_field_ident: #from_struct_ident.#old_field_ident, } } } } None => { - let field_ident = &self.inner.ident; + let field_ident = &*self.ident; + quote! { - #field_ident: #from_ident.#field_ident, + #field_ident: #from_struct_ident.#field_ident, } } } diff --git a/crates/stackable-versioned-macros/src/codegen/item/mod.rs b/crates/stackable-versioned-macros/src/codegen/item/mod.rs new file mode 100644 index 000000000..2c1d5f151 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/item/mod.rs @@ -0,0 +1,5 @@ +mod field; +pub(crate) use field::*; + +mod variant; +pub(crate) use variant::*; diff --git a/crates/stackable-versioned-macros/src/codegen/item/variant.rs b/crates/stackable-versioned-macros/src/codegen/item/variant.rs new file mode 100644 index 000000000..25352c79f --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/item/variant.rs @@ -0,0 +1,215 @@ +use std::collections::BTreeMap; + +use darling::{util::IdentString, FromVariant, Result}; +use k8s_version::Version; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote}; +use syn::{token::Not, Attribute, Fields, Type, TypeNever, Variant}; + +use crate::{ + attrs::item::VariantAttributes, + codegen::{ + changes::{BTreeMapExt, ChangesetExt}, + ItemStatus, VersionDefinition, + }, + utils::VariantIdent, +}; + +pub(crate) struct VersionedVariant { + pub(crate) original_attributes: Vec, + pub(crate) changes: Option>, + pub(crate) ident: VariantIdent, + pub(crate) fields: Fields, +} + +impl VersionedVariant { + pub(crate) fn new(variant: Variant, versions: &[VersionDefinition]) -> Result { + let variant_attributes = VariantAttributes::from_variant(&variant)?; + variant_attributes.validate_versions(versions)?; + + let variant_ident = VariantIdent::from(variant.ident); + + // FIXME (@Techassi): As we currently don't support enum variants with + // data, we just return the Never type as the code generation code for + // enum variants won't use this type information. + let ty = Type::Never(TypeNever { + bang_token: Not([Span::call_site()]), + }); + let changes = variant_attributes.common.into_changeset(&variant_ident, ty); + + Ok(Self { + original_attributes: variant_attributes.attrs, + fields: variant.fields, + ident: variant_ident, + changes, + }) + } + + pub(crate) fn insert_container_versions(&mut self, versions: &[VersionDefinition]) { + if let Some(changes) = &mut self.changes { + // FIXME (@Techassi): Support enum variants with data + let ty = Type::Never(TypeNever { + bang_token: Not([Span::call_site()]), + }); + + changes.insert_container_versions(versions, &ty); + } + } + + /// Generates tokens to be used in a container definition. + pub(crate) fn generate_for_container( + &self, + version: &VersionDefinition, + ) -> Option { + let original_attributes = &self.original_attributes; + let fields = &self.fields; + + match &self.changes { + // NOTE (@Techassi): https://rust-lang.github.io/rust-clippy/master/index.html#/expect_fun_call + Some(changes) => match changes.get(&version.inner).unwrap_or_else(|| { + panic!( + "internal error: chain must contain container version {}", + version.inner + ) + }) { + ItemStatus::Addition { ident, .. } => Some(quote! { + #(#original_attributes)* + #ident #fields, + }), + ItemStatus::Change { to_ident, .. } => Some(quote! { + #(#original_attributes)* + #to_ident #fields, + }), + ItemStatus::Deprecation { ident, 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. + let deprecated_attr = if let Some(note) = note { + quote! {#[deprecated = #note]} + } else { + quote! {#[deprecated]} + }; + + Some(quote! { + #(#original_attributes)* + #deprecated_attr + #ident #fields, + }) + } + ItemStatus::NoChange { + previously_deprecated, + ident, + .. + } => { + // TODO (@Techassi): Also carry along the deprecation + // note. + let deprecated_attr = previously_deprecated.then(|| quote! {#[deprecated]}); + + Some(quote! { + #(#original_attributes)* + #deprecated_attr + #ident #fields, + }) + } + ItemStatus::NotPresent => None, + }, + None => { + // If there is no chain of variant actions, the variant is not + // versioned and code generation is straight forward. + // Unversioned variants are always included in versioned enums. + let ident = &self.ident; + + Some(quote! { + #(#original_attributes)* + #ident #fields, + }) + } + } + } + + pub(crate) fn generate_for_from_impl( + &self, + version: &VersionDefinition, + next_version: &VersionDefinition, + enum_ident: &IdentString, + ) -> Option { + let variant_fields = match &self.fields { + Fields::Named(fields_named) => { + let fields: Vec<_> = fields_named + .named + .iter() + .map(|field| { + field + .ident + .as_ref() + .expect("named fields always have an ident") + .clone() + }) + .collect(); + + quote! { { #(#fields),* } } + } + Fields::Unnamed(fields_unnamed) => { + let fields: Vec<_> = fields_unnamed + .unnamed + .iter() + .enumerate() + .map(|(index, _)| format_ident!("__sv_{index}")) + .collect(); + + quote! { ( #(#fields),* ) } + } + Fields::Unit => TokenStream::new(), + }; + + match &self.changes { + Some(changes) => { + let next_change = changes.get_expect(&next_version.inner); + let change = changes.get_expect(&version.inner); + + match (change, next_change) { + (_, ItemStatus::Addition { .. }) => None, + (old, next) => { + let next_version_ident = &next_version.ident; + let old_version_ident = &version.ident; + + let next_variant_ident = next.get_ident(); + let old_variant_ident = old.get_ident(); + + let old = quote! { + #old_version_ident::#enum_ident::#old_variant_ident #variant_fields + }; + let next = quote! { + #next_version_ident::#enum_ident::#next_variant_ident #variant_fields + }; + + Some(quote! { + #old => #next, + }) + } + } + } + None => { + let next_version_ident = &next_version.ident; + let old_version_ident = &version.ident; + let variant_ident = &*self.ident; + + let old = quote! { + #old_version_ident::#enum_ident::#variant_ident #variant_fields + }; + let next = quote! { + #next_version_ident::#enum_ident::#variant_ident #variant_fields + }; + + Some(quote! { + #old => #next, + }) + } + } + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/mod.rs b/crates/stackable-versioned-macros/src/codegen/mod.rs index 77c617a04..87a975a9f 100644 --- a/crates/stackable-versioned-macros/src/codegen/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/mod.rs @@ -1,5 +1,141 @@ -pub(crate) mod chain; -pub(crate) mod common; -pub(crate) mod venum; -pub(crate) mod vmod; -pub(crate) mod vstruct; +use darling::util::IdentString; +use k8s_version::Version; +use quote::format_ident; +use syn::{Path, Type}; + +use crate::attrs::{container::StandaloneContainerAttributes, module::ModuleAttributes}; + +pub(crate) mod changes; +pub(crate) mod container; +pub(crate) mod item; +pub(crate) mod module; + +#[derive(Debug)] +pub(crate) struct VersionDefinition { + /// Indicates that the container version is deprecated. + pub(crate) deprecated: bool, + + /// Indicates that the generation of `From for NEW` should be skipped. + pub(crate) skip_from: bool, + + /// A validated Kubernetes API version. + pub(crate) inner: Version, + + /// The ident of the container. + pub(crate) ident: IdentString, + + /// Store additional doc-comment lines for this version. + pub(crate) docs: Vec, +} + +// NOTE (@Techassi): Can we maybe unify these two impls? +impl From<&StandaloneContainerAttributes> for Vec { + fn from(attributes: &StandaloneContainerAttributes) -> Self { + attributes + .common_root_arguments + .versions + .iter() + .map(|v| VersionDefinition { + skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()), + ident: format_ident!("{version}", version = v.name.to_string()).into(), + deprecated: v.deprecated.is_present(), + docs: process_docs(&v.doc), + inner: v.name, + }) + .collect() + } +} + +impl From<&ModuleAttributes> for Vec { + fn from(attributes: &ModuleAttributes) -> Self { + attributes + .common_root_arguments + .versions + .iter() + .map(|v| VersionDefinition { + skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()), + ident: format_ident!("{version}", version = v.name.to_string()).into(), + deprecated: v.deprecated.is_present(), + docs: process_docs(&v.doc), + inner: v.name, + }) + .collect() + } +} + +#[derive(Debug, PartialEq)] +pub(crate) enum ItemStatus { + Addition { + ident: IdentString, + default_fn: Path, + // NOTE (@Techassi): We need to carry idents and type information in + // nearly every status. Ideally, we would store this in separate maps. + ty: Type, + }, + Change { + from_ident: IdentString, + to_ident: IdentString, + from_type: Type, + to_type: Type, + }, + Deprecation { + previous_ident: IdentString, + note: Option, + ident: IdentString, + }, + NoChange { + previously_deprecated: bool, + ident: IdentString, + ty: Type, + }, + NotPresent, +} + +impl ItemStatus { + pub(crate) fn get_ident(&self) -> &IdentString { + match &self { + ItemStatus::Addition { ident, .. } => ident, + ItemStatus::Change { to_ident, .. } => to_ident, + ItemStatus::Deprecation { ident, .. } => ident, + ItemStatus::NoChange { ident, .. } => ident, + ItemStatus::NotPresent => unreachable!(), + } + } +} + +pub(crate) struct Change { + pub(crate) item_ident: IdentString, + pub(crate) item_type: Type, + pub(crate) ty: ChangeType, +} + +pub(crate) enum ChangeType { + Added { + default_fn: Path, + }, + Changed { + from_ident: IdentString, + from_type: Type, + }, + Deprecated { + from_ident: IdentString, + note: Option, + }, +} + +/// Converts lines of doc-comments into a trimmed list. +fn process_docs(input: &Option) -> Vec { + if let Some(input) = input { + input + // Trim the leading and trailing whitespace, deleting superfluous + // empty lines. + .trim() + .lines() + // Trim the leading and trailing whitespace on each line that can be + // introduced when the developer indents multi-line comments. + .map(|line| line.trim().to_owned()) + .collect() + } else { + Vec::new() + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/module.rs b/crates/stackable-versioned-macros/src/codegen/module.rs new file mode 100644 index 000000000..96f577d9f --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/module.rs @@ -0,0 +1,92 @@ +use darling::util::IdentString; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{token::Pub, Ident, Visibility}; + +use crate::codegen::{container::Container, VersionDefinition}; + +pub(crate) struct ModuleInput { + pub(crate) vis: Visibility, + pub(crate) ident: Ident, +} + +pub(crate) struct Module { + versions: Vec, + containers: Vec, + preserve_module: bool, + ident: IdentString, + vis: Visibility, +} + +impl Module { + pub(crate) fn new( + ModuleInput { ident, vis, .. }: ModuleInput, + preserve_module: bool, + versions: Vec, + containers: Vec, + ) -> Self { + Self { + ident: ident.into(), + preserve_module, + containers, + versions, + vis, + } + } + + pub(crate) fn generate_tokens(&self) -> TokenStream { + if self.containers.is_empty() { + return quote! {}; + } + + // TODO (@Techassi): Leave comment explaining this + let version_module_vis = if self.preserve_module { + &Visibility::Public(Pub::default()) + } else { + &self.vis + }; + + let mut tokens = TokenStream::new(); + + let module_ident = &self.ident; + let module_vis = &self.vis; + + let mut versions = self.versions.iter().peekable(); + + while let Some(version) = versions.next() { + let mut container_definitions = TokenStream::new(); + let mut from_impls = TokenStream::new(); + + let version_ident = &version.ident; + + for container in &self.containers { + container_definitions.extend(container.generate_definition(version)); + from_impls.extend(container.generate_from_impl( + version, + versions.peek().copied(), + true, + )); + } + + tokens.extend(quote! { + #version_module_vis mod #version_ident { + use super::*; + + #container_definitions + } + + #from_impls + }); + } + + if self.preserve_module { + quote! { + #module_vis mod #module_ident { + #tokens + } + } + } else { + tokens + } + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/venum/mod.rs b/crates/stackable-versioned-macros/src/codegen/venum/mod.rs deleted file mode 100644 index 4b9c5bdc3..000000000 --- a/crates/stackable-versioned-macros/src/codegen/venum/mod.rs +++ /dev/null @@ -1,251 +0,0 @@ -use std::ops::Deref; - -use itertools::Itertools; -use proc_macro2::TokenStream; -use quote::quote; -use syn::{punctuated::Punctuated, token::Comma, Error, Variant}; - -use crate::{ - attrs::common::StandaloneContainerAttributes, - codegen::{ - chain::Neighbors, - common::{ - generate_module, Container, ContainerInput, Item, ItemStatus, VersionDefinition, - VersionedContainer, - }, - venum::variant::VersionedVariant, - }, -}; - -pub(crate) mod variant; - -pub(crate) struct GenerateVersionTokens { - from_impl: Option, - enum_definition: TokenStream, -} - -/// Stores individual versions of a single enum. Each version tracks variant -/// actions, which describe if the variant was added, renamed or deprecated in -/// that version. Variants which are not versioned, are included in every -/// version of the enum. -#[derive(Debug)] -pub(crate) struct VersionedEnum(VersionedContainer); - -impl Deref for VersionedEnum { - type Target = VersionedContainer; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Container, VersionedVariant> for VersionedEnum { - fn new( - input: ContainerInput, - variants: Punctuated, - attributes: StandaloneContainerAttributes, - ) -> syn::Result { - let ident = &input.ident; - - // Convert the raw version attributes into a container version. - let versions: Vec<_> = (&attributes).into(); - - // Extract the attributes for every variant from the raw token - // stream and also validate that each variant action version uses a - // version declared by the container attribute. - let mut items = Vec::new(); - - for variant in variants { - let mut versioned_field = VersionedVariant::new(variant, &attributes)?; - versioned_field.insert_container_versions(&versions); - items.push(versioned_field); - } - - // Check for field ident collisions - for version in &versions { - // Collect the idents of all variants for a single version and then - // ensure that all idents are unique. If they are not, return an - // error. - - // TODO (@Techassi): Report which variant(s) use a duplicate ident and - // also hint what can be done to fix it based on the variant action / - // status. - - if !items.iter().map(|f| f.get_ident(version)).all_unique() { - return Err(Error::new( - ident.span(), - format!("Enum contains renamed variants which collide with other variants in version {version}", version = version.inner), - )); - } - } - - Ok(Self(VersionedContainer::new( - input, attributes, versions, items, - ))) - } - - fn generate_standalone_tokens(&self) -> TokenStream { - let mut tokens = TokenStream::new(); - let mut versions = self.versions.iter().peekable(); - - while let Some(version) = versions.next() { - let GenerateVersionTokens { - enum_definition, - from_impl, - } = self.generate_version(version, versions.peek().copied()); - - let module_definition = generate_module(version, &self.visibility, enum_definition); - - tokens.extend(module_definition); - tokens.extend(from_impl); - } - - tokens - } - - fn generate_nested_tokens(&self) -> TokenStream { - quote! {} - } -} - -impl VersionedEnum { - fn generate_version( - &self, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - ) -> GenerateVersionTokens { - let mut enum_definition = TokenStream::new(); - - let original_attributes = &self.original_attributes; - let enum_name = &self.idents.original; - - // Generate variants of the enum for `version`. - let variants = self.generate_enum_variants(version); - - // Generate doc comments for the container (enum) - let version_specific_docs = self.generate_enum_docs(version); - - // Generate enum definition tokens - enum_definition.extend(quote! { - #version_specific_docs - #(#original_attributes)* - pub enum #enum_name { - #variants - } - }); - - let from_impl = if !self.options.skip_from && !version.skip_from { - self.generate_from_impl(version, next_version) - } else { - None - }; - - GenerateVersionTokens { - enum_definition, - from_impl, - } - } - - /// Generates version specific doc comments for the enum. - fn generate_enum_docs(&self, version: &VersionDefinition) -> TokenStream { - let mut tokens = TokenStream::new(); - - for (i, doc) in version.version_specific_docs.iter().enumerate() { - if i == 0 { - // Prepend an empty line to clearly separate the version - // specific docs. - tokens.extend(quote! { - #[doc = ""] - }) - } - tokens.extend(quote! { - #[doc = #doc] - }) - } - - tokens - } - - fn generate_enum_variants(&self, version: &VersionDefinition) -> TokenStream { - let mut token_stream = TokenStream::new(); - - for variant in &self.items { - token_stream.extend(variant.generate_for_container(version)); - } - - token_stream - } - - fn generate_from_impl( - &self, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - ) -> Option { - if let Some(next_version) = next_version { - let next_module_name = &next_version.ident; - let module_name = &version.ident; - - let enum_ident = &self.idents.original; - let from_ident = &self.idents.from; - - let mut variants = TokenStream::new(); - - for item in &self.items { - variants.extend(item.generate_for_from_impl( - module_name, - next_module_name, - version, - next_version, - enum_ident, - )) - } - - // 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 - || next_version.deprecated - || self.is_any_variant_deprecated(version) - || self.is_any_variant_deprecated(next_version)) - .then_some(quote! { #[allow(deprecated)] }); - - return Some(quote! { - #[automatically_derived] - #allow_attribute - impl ::std::convert::From<#module_name::#enum_ident> for #next_module_name::#enum_ident { - fn from(#from_ident: #module_name::#enum_ident) -> Self { - match #from_ident { - #variants - } - } - } - }); - } - - None - } - - /// Returns whether any field is deprecated in the provided - /// [`ContainerVersion`]. - fn is_any_variant_deprecated(&self, version: &VersionDefinition) -> bool { - // First, iterate over all fields. Any will return true if any of the - // function invocations return true. If a field doesn't have a chain, - // we can safely default to false (unversioned fields cannot be - // deprecated). Then we retrieve the status of the field and ensure it - // is deprecated. - self.items.iter().any(|f| { - f.chain.as_ref().map_or(false, |c| { - c.value_is(&version.inner, |a| { - matches!( - a, - ItemStatus::Deprecation { .. } - | ItemStatus::NoChange { - previously_deprecated: true, - .. - } - ) - }) - }) - }) - } -} diff --git a/crates/stackable-versioned-macros/src/codegen/venum/variant.rs b/crates/stackable-versioned-macros/src/codegen/venum/variant.rs deleted file mode 100644 index 81b824091..000000000 --- a/crates/stackable-versioned-macros/src/codegen/venum/variant.rs +++ /dev/null @@ -1,246 +0,0 @@ -use std::ops::{Deref, DerefMut}; - -use darling::{util::IdentString, FromVariant}; -use proc_macro2::{Span, TokenStream}; -use quote::{format_ident, quote}; -use syn::{token::Not, Ident, Type, TypeNever, Variant}; - -use crate::{ - attrs::{ - common::{ItemAttributes, StandaloneContainerAttributes}, - variant::VariantAttributes, - }, - codegen::{ - chain::BTreeMapExt, - common::{ - remove_deprecated_variant_prefix, Attributes, InnerItem, Item, ItemStatus, Named, - VersionDefinition, VersionedItem, - }, - }, -}; - -/// A versioned variant, which contains contains common [`Variant`] data and a -/// chain of actions. -/// -/// The chain of action maps versions to an action and the appropriate variant -/// name. Additionally, the [`Variant`] data can be used to forward attributes, -/// generate documentation, etc. -#[derive(Debug)] -pub(crate) struct VersionedVariant(VersionedItem); - -impl Deref for VersionedVariant { - type Target = VersionedItem; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for VersionedVariant { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl TryFrom<&Variant> for VariantAttributes { - type Error = darling::Error; - - fn try_from(variant: &Variant) -> Result { - Self::from_variant(variant) - } -} - -impl Attributes for VariantAttributes { - fn common_attributes_owned(self) -> ItemAttributes { - self.common - } - - fn common_attributes(&self) -> &ItemAttributes { - &self.common - } - - fn original_attributes(&self) -> &Vec { - &self.attrs - } -} - -impl InnerItem for Variant { - fn ty(&self) -> syn::Type { - // FIXME (@Techassi): As we currently don't support enum variants with - // data, we just return the Never type as the code generation code for - // enum variants won't use this type information. - Type::Never(TypeNever { - bang_token: Not([Span::call_site()]), - }) - } -} - -impl Named for Variant { - fn cleaned_ident(&self) -> Ident { - remove_deprecated_variant_prefix(self.ident()) - } - - fn ident(&self) -> &Ident { - &self.ident - } -} - -impl VersionedVariant { - /// Creates a new versioned variant. - /// - /// Internally this calls [`VersionedItem::new`] to handle most of the - /// common creation code. - pub(crate) fn new( - variant: Variant, - container_attributes: &StandaloneContainerAttributes, - ) -> syn::Result { - let item = VersionedItem::<_, VariantAttributes>::new(variant, container_attributes)?; - Ok(Self(item)) - } - - /// Generates tokens to be used in a container definition. - pub(crate) fn generate_for_container( - &self, - container_version: &VersionDefinition, - ) -> Option { - let original_attributes = &self.original_attributes; - let fields = &self.inner.fields; - - match &self.chain { - // NOTE (@Techassi): https://rust-lang.github.io/rust-clippy/master/index.html#/expect_fun_call - Some(chain) => match chain.get(&container_version.inner).unwrap_or_else(|| { - panic!( - "internal error: chain must contain container version {}", - container_version.inner - ) - }) { - ItemStatus::Addition { ident, .. } => Some(quote! { - #(#original_attributes)* - #ident #fields, - }), - ItemStatus::Change { to_ident, .. } => Some(quote! { - #(#original_attributes)* - #to_ident #fields, - }), - ItemStatus::Deprecation { ident, 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. - let deprecated_attr = if let Some(note) = note { - quote! {#[deprecated = #note]} - } else { - quote! {#[deprecated]} - }; - - Some(quote! { - #(#original_attributes)* - #deprecated_attr - #ident #fields, - }) - } - ItemStatus::NoChange { - previously_deprecated, - ident, - .. - } => { - // TODO (@Techassi): Also carry along the deprecation - // note. - let deprecated_attr = previously_deprecated.then(|| quote! {#[deprecated]}); - - Some(quote! { - #(#original_attributes)* - #deprecated_attr - #ident #fields, - }) - } - ItemStatus::NotPresent => None, - }, - None => { - // If there is no chain of variant actions, the variant is not - // versioned and code generation is straight forward. - // Unversioned variants are always included in versioned enums. - let ident = &self.inner.ident; - - Some(quote! { - #(#original_attributes)* - #ident #fields, - }) - } - } - } - - /// Generates tokens to be used in a [`From`] implementation. - pub(crate) fn generate_for_from_impl( - &self, - module_name: &Ident, - next_module_name: &Ident, - version: &VersionDefinition, - next_version: &VersionDefinition, - enum_ident: &IdentString, - ) -> TokenStream { - let variant_data = match &self.inner.fields { - syn::Fields::Named(fields_named) => { - let field_names = fields_named - .named - .iter() - .map(|field| { - field - .ident - .as_ref() - .expect("named fields always have an ident") - .clone() - }) - .collect::>(); - - let tokens = quote! { { #(#field_names),* } }; - tokens - } - syn::Fields::Unnamed(fields_unnamed) => { - let field_names = fields_unnamed - .unnamed - .iter() - .enumerate() - .map(|(index, _)| format_ident!("__sv_{index}")) - .collect::>(); - - let tokens = quote! { ( #(#field_names),* ) }; - tokens - } - - syn::Fields::Unit => TokenStream::new(), - }; - - match &self.chain { - Some(chain) => match ( - chain.get_expect(&version.inner), - chain.get_expect(&next_version.inner), - ) { - (_, ItemStatus::Addition { .. }) => quote! {}, - (old, next) => { - let old_variant_ident = old - .get_ident() - .expect("internal error: old variant must have a name"); - let next_variant_ident = next - .get_ident() - .expect("internal error: next variant must have a name"); - - quote! { - #module_name::#enum_ident::#old_variant_ident #variant_data => #next_module_name::#enum_ident::#next_variant_ident #variant_data, - } - } - }, - None => { - let variant_ident = &self.inner.ident; - - quote! { - #module_name::#enum_ident::#variant_ident #variant_data => #next_module_name::#enum_ident::#variant_ident #variant_data, - } - } - } - } -} diff --git a/crates/stackable-versioned-macros/src/codegen/vmod/mod.rs b/crates/stackable-versioned-macros/src/codegen/vmod/mod.rs deleted file mode 100644 index c4ae62a4e..000000000 --- a/crates/stackable-versioned-macros/src/codegen/vmod/mod.rs +++ /dev/null @@ -1,51 +0,0 @@ -use darling::FromAttributes; -use proc_macro2::TokenStream; -use quote::quote; -use syn::{spanned::Spanned, Error, Item, ItemMod, Result}; - -use crate::{ - attrs::common::{ModuleAttributes, NestedContainerAttributes}, - codegen::{common::VersionDefinition, venum::VersionedEnum, vstruct::VersionedStruct}, -}; - -pub(crate) struct VersionedModule { - module: ItemMod, - // TODO (@Techassi): This will change - attributes: ModuleAttributes, -} - -pub(crate) enum ModuleItem { - Struct(VersionedStruct), - Enum(VersionedEnum), -} - -impl VersionedModule { - pub(crate) fn new(module: ItemMod, attributes: ModuleAttributes) -> Result { - let versions: Vec = (&attributes).into(); - - let Some((_, items)) = &module.content else { - return Err(Error::new(module.span(), "module cannot be empty")); - }; - - // let mut versioned_items = Vec::new(); - - for item in items { - match item { - Item::Enum(item_enum) => { - let module_item_attributes = - NestedContainerAttributes::from_attributes(&item_enum.attrs)?; - // let versioned_enum = VersionedEnum::new(module_item_attributes) - // versioned_item.push(ModuleItem(versioned_enum)) - } - Item::Struct(item_struct) => todo!(), - _ => todo!(), - } - } - - Ok(VersionedModule { module, attributes }) - } - - pub(crate) fn generate_tokens(&self) -> TokenStream { - quote! {} - } -} diff --git a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs b/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs deleted file mode 100644 index d4b966087..000000000 --- a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs +++ /dev/null @@ -1,489 +0,0 @@ -use std::ops::Deref; - -use darling::util::IdentString; -use itertools::Itertools; -use proc_macro2::TokenStream; -use quote::quote; -use syn::{parse_quote, Error, Fields, Ident}; - -use crate::{ - attrs::common::StandaloneContainerAttributes, - codegen::{ - chain::Neighbors, - common::{ - generate_module, Container, ContainerInput, Item, ItemStatus, VersionDefinition, - VersionExt, VersionedContainer, - }, - vstruct::field::VersionedField, - }, -}; - -pub(crate) mod field; - -pub(crate) struct GenerateVersionTokens { - kubernetes_definition: Option, - struct_definition: TokenStream, - from_impl: Option, -} - -pub(crate) struct KubernetesTokens { - merged_crd_fn_call: TokenStream, - variant_display: String, - enum_variant: Ident, -} - -/// Stores individual versions of a single struct. Each version tracks field -/// actions, which describe if the field was added, renamed or deprecated in -/// that version. Fields which are not versioned, are included in every -/// version of the struct. -#[derive(Debug)] -pub(crate) struct VersionedStruct(VersionedContainer); - -impl Deref for VersionedStruct { - type Target = VersionedContainer; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Container for VersionedStruct { - fn new( - input: ContainerInput, - fields: Fields, - attributes: StandaloneContainerAttributes, - ) -> syn::Result { - let ident = &input.ident; - - // Convert the raw version attributes into a container version. - let versions: Vec<_> = (&attributes).into(); - - // Extract the field attributes for every field from the raw token - // stream and also validate that each field action version uses a - // version declared by the container attribute. - let mut items = Vec::new(); - - for field in fields { - let mut versioned_field = VersionedField::new(field, &attributes)?; - versioned_field.insert_container_versions(&versions); - items.push(versioned_field); - } - - // Check for field ident collisions - for version in &versions { - // Collect the idents of all fields for a single version and then - // ensure that all idents are unique. If they are not, return an - // error. - - // TODO (@Techassi): Report which field(s) use a duplicate ident and - // also hint what can be done to fix it based on the field action / - // status. - - if !items.iter().map(|f| f.get_ident(version)).all_unique() { - return Err(Error::new( - ident.span(), - format!("struct contains renamed fields which collide with other fields in version {version}", version = version.inner), - )); - } - } - - // Validate K8s specific requirements - // Ensure that the struct name includes the 'Spec' suffix. - if attributes.kubernetes_args.is_some() && !ident.to_string().ends_with("Spec") { - return Err(Error::new( - ident.span(), - "struct name needs to include the `Spec` suffix if Kubernetes features are enabled via `#[versioned(k8s())]`" - )); - } - - Ok(Self(VersionedContainer::new( - input, attributes, versions, items, - ))) - } - - fn generate_standalone_tokens(&self) -> TokenStream { - let mut kubernetes_definitions = Vec::new(); - let mut tokens = TokenStream::new(); - - let mut versions = self.versions.iter().peekable(); - - while let Some(version) = versions.next() { - // Generate the container definition, from implementation and Kubernetes related tokens - // for that particular version. - let GenerateVersionTokens { - struct_definition, - from_impl, - kubernetes_definition, - } = self.generate_version(version, versions.peek().copied()); - - let module_definition = generate_module(version, &self.visibility, struct_definition); - - if let Some(kubernetes_definition) = kubernetes_definition { - kubernetes_definitions.push(kubernetes_definition); - } - - tokens.extend(module_definition); - tokens.extend(from_impl); - } - - if !kubernetes_definitions.is_empty() { - tokens.extend(self.generate_kubernetes_merge_crds(kubernetes_definitions)); - } - - tokens - } - - fn generate_nested_tokens(&self) -> TokenStream { - quote! {} - } -} - -impl VersionedStruct { - /// Generates all tokens for a single instance of a versioned struct. - /// - /// This functions returns a value triple containing various pieces of generated code which can - /// be combined in multiple ways to allow generate the correct code based on which mode we are - /// running: "standalone" or "nested". - /// - /// # Struct Definition - /// - /// The first value of the triple contains the struct definition including all attributes and - /// macros it needs. These tokens **do not** include the wrapping module indicating to which - /// version this definition belongs. This is done deliberately to enable grouping multiple - /// versioned containers when running in "nested" mode. - /// - /// # From Implementation - /// - /// The second value contains the [`From`] implementation which enables conversion from _this_ - /// version to the _next_ one. These tokens need to be placed outside the version modules, - /// because they reference the structs using the version modules, like `v1alpha1` and `v1beta1`. - /// - /// # Kubernetes-specific Code - /// - /// The last value contains Kubernetes specific data. Currently, it contains data to generate - /// code to enable merging CRDs. - fn generate_version( - &self, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - ) -> GenerateVersionTokens { - let original_attributes = &self.original_attributes; - let struct_name = &self.idents.original; - - // Generate fields of the struct for `version`. - let fields = self.generate_struct_fields(version); - - // Generate doc comments for the container (struct) - let version_specific_docs = self.generate_struct_docs(version); - - // Generate K8s specific code - let (kubernetes_cr_derive, kubernetes_definition) = match &self.options.kubernetes_options { - Some(options) => { - // Generate the CustomResource derive macro with the appropriate - // attributes supplied using #[kube()]. - let cr_derive = self.generate_kubernetes_cr_derive(version); - - // Generate merged_crd specific code when not opted out. - let merged_crd = if !options.skip_merged_crd { - let merged_crd_fn_call = self.generate_kubernetes_crd_fn_call(version); - let enum_variant = version.inner.as_variant_ident(); - let variant_display = version.inner.to_string(); - - Some(KubernetesTokens { - merged_crd_fn_call, - variant_display, - enum_variant, - }) - } else { - None - }; - - (Some(cr_derive), merged_crd) - } - None => (None, None), - }; - - // Generate struct definition tokens - let struct_definition = quote! { - #version_specific_docs - #(#original_attributes)* - #kubernetes_cr_derive - pub struct #struct_name { - #fields - } - }; - - // Generate the From impl between this `version` and the next one. - let from_impl = if !self.options.skip_from && !version.skip_from { - self.generate_from_impl(version, next_version) - } else { - None - }; - - GenerateVersionTokens { - kubernetes_definition, - struct_definition, - from_impl, - } - } - - /// Generates version specific doc comments for the struct. - fn generate_struct_docs(&self, version: &VersionDefinition) -> TokenStream { - let mut tokens = TokenStream::new(); - - for (i, doc) in version.version_specific_docs.iter().enumerate() { - if i == 0 { - // Prepend an empty line to clearly separate the version - // specific docs. - tokens.extend(quote! { - #[doc = ""] - }) - } - tokens.extend(quote! { - #[doc = #doc] - }) - } - - tokens - } - - /// Generates struct fields following the `name: type` format which includes - /// a trailing comma. - fn generate_struct_fields(&self, version: &VersionDefinition) -> TokenStream { - let mut tokens = TokenStream::new(); - - for item in &self.items { - tokens.extend(item.generate_for_container(version)); - } - - tokens - } - - /// Generates the [`From`] impl which enables conversion between a version - /// and the next one. - fn generate_from_impl( - &self, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - ) -> Option { - if let Some(next_version) = next_version { - let next_module_name = &next_version.ident; - let module_name = &version.ident; - - let struct_ident = &self.idents.original; - let from_ident = &self.idents.from; - - let fields = self.generate_from_fields(version, next_version, from_ident); - - // Include allow(deprecated) only when this or the next version is - // deprecated. Also include it, when a field in this or the next - // version is deprecated. - let allow_attribute = (version.deprecated - || next_version.deprecated - || self.is_any_field_deprecated(version) - || self.is_any_field_deprecated(next_version)) - .then_some(quote! { #[allow(deprecated)] }); - - return Some(quote! { - #[automatically_derived] - #allow_attribute - impl ::std::convert::From<#module_name::#struct_ident> for #next_module_name::#struct_ident { - fn from(#from_ident: #module_name::#struct_ident) -> Self { - Self { - #fields - } - } - } - }); - } - - None - } - - /// Generates fields used in the [`From`] impl following the - /// `new_name: struct_name.old_name` format which includes a trailing comma. - fn generate_from_fields( - &self, - version: &VersionDefinition, - next_version: &VersionDefinition, - from_ident: &IdentString, - ) -> TokenStream { - let mut token_stream = TokenStream::new(); - - for item in &self.items { - token_stream.extend(item.generate_for_from_impl(version, next_version, from_ident)) - } - - token_stream - } - - /// Returns whether any field is deprecated in the provided - /// [`ContainerVersion`]. - fn is_any_field_deprecated(&self, version: &VersionDefinition) -> bool { - // First, iterate over all fields. Any will return true if any of the - // function invocations return true. If a field doesn't have a chain, - // we can safely default to false (unversioned fields cannot be - // deprecated). Then we retrieve the status of the field and ensure it - // is deprecated. - self.items.iter().any(|f| { - f.chain.as_ref().map_or(false, |c| { - c.value_is(&version.inner, |a| { - matches!( - a, - ItemStatus::Deprecation { .. } - | ItemStatus::NoChange { - previously_deprecated: true, - .. - } - ) - }) - }) - }) - } -} - -// Kubernetes specific code generation -impl VersionedStruct { - /// Generates the `kube::CustomResource` derive with the appropriate macro - /// attributes. - fn generate_kubernetes_cr_derive(&self, version: &VersionDefinition) -> Option { - if let Some(kubernetes_options) = &self.options.kubernetes_options { - // Required arguments - let group = &kubernetes_options.group; - let version = version.inner.to_string(); - let kind = kubernetes_options - .kind - .as_ref() - .map_or(self.idents.kubernetes.to_string(), |kind| kind.clone()); - - // Optional arguments - let namespaced = kubernetes_options - .namespaced - .then_some(quote! { , namespaced }); - let singular = kubernetes_options - .singular - .as_ref() - .map(|s| quote! { , singular = #s }); - let plural = kubernetes_options - .plural - .as_ref() - .map(|p| quote! { , plural = #p }); - - return Some(quote! { - #[derive(::kube::CustomResource)] - #[kube(group = #group, version = #version, kind = #kind #singular #plural #namespaced)] - }); - } - - None - } - - /// Generates the `merge_crds` function call. - fn generate_kubernetes_merge_crds( - &self, - kubernetes_definitions: Vec, - ) -> TokenStream { - let enum_ident = &self.idents.kubernetes; - let enum_vis = &self.visibility; - - let mut enum_display_impl_matches = TokenStream::new(); - let mut enum_variant_idents = TokenStream::new(); - let mut merged_crd_fn_calls = TokenStream::new(); - - for KubernetesTokens { - merged_crd_fn_call, - variant_display, - enum_variant, - } in kubernetes_definitions - { - merged_crd_fn_calls.extend(quote! { - #merged_crd_fn_call, - }); - - enum_variant_idents.extend(quote! { - #enum_variant, - }); - - enum_display_impl_matches.extend(quote! { - #enum_ident::#enum_variant => f.write_str(#variant_display), - }); - } - - quote! { - #[automatically_derived] - #enum_vis enum #enum_ident { - #enum_variant_idents - } - - #[automatically_derived] - impl ::std::fmt::Display for #enum_ident { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { - match self { - #enum_display_impl_matches - } - } - } - - #[automatically_derived] - impl #enum_ident { - /// Generates a merged CRD which contains all versions defined using the `#[versioned()]` macro. - pub fn merged_crd( - stored_apiversion: Self - ) -> ::std::result::Result<::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError> { - ::kube::core::crd::merge_crds(vec![#merged_crd_fn_calls], &stored_apiversion.to_string()) - } - - /// Generates and writes a merged CRD which contains all versions defined using the `#[versioned()]` - /// macro to a file located at `path`. - pub fn write_merged_crd

(path: P, stored_apiversion: Self, operator_version: &str) -> Result<(), ::stackable_versioned::Error> - where P: AsRef<::std::path::Path> - { - use ::stackable_shared::yaml::{YamlSchema, SerializeOptions}; - - let merged_crd = Self::merged_crd(stored_apiversion).map_err(|err| ::stackable_versioned::Error::MergeCrd { - source: err, - })?; - - YamlSchema::write_yaml_schema( - &merged_crd, - path, - operator_version, - SerializeOptions::default() - ).map_err(|err| ::stackable_versioned::Error::SerializeYaml { - source: err, - }) - } - - /// Generates and writes a merged CRD which contains all versions defined using the `#[versioned()]` - /// macro to stdout. - pub fn print_merged_crd(stored_apiversion: Self, operator_version: &str) -> Result<(), ::stackable_versioned::Error> { - use ::stackable_shared::yaml::{YamlSchema, SerializeOptions}; - - let merged_crd = Self::merged_crd(stored_apiversion).map_err(|err| ::stackable_versioned::Error::MergeCrd { - source: err, - })?; - - YamlSchema::print_yaml_schema( - &merged_crd, - operator_version, - SerializeOptions::default() - ).map_err(|err| ::stackable_versioned::Error::SerializeYaml { - source: err, - }) - } - } - } - } - - /// Generates the inner `crd()` functions calls which get used in the - /// `merge_crds` function. - fn generate_kubernetes_crd_fn_call(&self, version: &VersionDefinition) -> TokenStream { - let struct_ident = &self.idents.kubernetes; - let version_ident = &version.ident; - let path: syn::Path = parse_quote!(#version_ident::#struct_ident); - - quote! { - <#path as ::kube::CustomResourceExt>::crd() - } - } -} diff --git a/crates/stackable-versioned-macros/src/consts.rs b/crates/stackable-versioned-macros/src/consts.rs deleted file mode 100644 index d064c344d..000000000 --- a/crates/stackable-versioned-macros/src/consts.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) const DEPRECATED_VARIANT_PREFIX: &str = "Deprecated"; -pub(crate) const DEPRECATED_FIELD_PREFIX: &str = "deprecated_"; diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs index 5eaaca4d8..b3730bd24 100644 --- a/crates/stackable-versioned-macros/src/lib.rs +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -2,11 +2,13 @@ use darling::{ast::NestedMeta, FromMeta}; use proc_macro::TokenStream; use syn::{spanned::Spanned, Error, Item}; -use crate::codegen::{ - common::{Container, ContainerInput}, - venum::VersionedEnum, - vmod::VersionedModule, - vstruct::VersionedStruct, +use crate::{ + attrs::{container::StandaloneContainerAttributes, module::ModuleAttributes}, + codegen::{ + container::{Container, StandaloneContainer}, + module::{Module, ModuleInput}, + VersionDefinition, + }, }; #[cfg(test)] @@ -14,7 +16,7 @@ mod test_utils; mod attrs; mod codegen; -mod consts; +mod utils; /// This macro enables generating versioned structs and enums. /// @@ -506,49 +508,78 @@ fn versioned_impl(attrs: proc_macro2::TokenStream, input: Item) -> proc_macro2:: match input { Item::Mod(item_mod) => { - let module_attributes = match parse_outer_attributes(attrs) { + let module_attributes: ModuleAttributes = match parse_outer_attributes(attrs) { Ok(ma) => ma, Err(err) => return err.write_errors(), }; - match VersionedModule::new(item_mod, module_attributes) { - Ok(versioned_module) => versioned_module.generate_tokens(), - Err(err) => Error::into_compile_error(err), - } - } - Item::Enum(item_enum) => { - let container_attributes = match parse_outer_attributes(attrs) { - Ok(ca) => ca, - Err(err) => return err.write_errors(), + let versions: Vec = (&module_attributes).into(); + let preserve_modules = module_attributes.preserve_module.is_present(); + + let module_span = item_mod.span(); + let module_input = ModuleInput { + ident: item_mod.ident, + vis: item_mod.vis, }; - let input = ContainerInput { - original_attributes: item_enum.attrs, - visibility: item_enum.vis, - ident: item_enum.ident, + let Some((_, items)) = item_mod.content else { + return Error::new(module_span, "the macro can only be used on module blocks") + .into_compile_error(); }; - match VersionedEnum::new(input, item_enum.variants, container_attributes) { - Ok(versioned_enum) => versioned_enum.generate_standalone_tokens(), - Err(err) => Error::into_compile_error(err), + let mut containers = Vec::new(); + + for item in items { + let container = match item { + Item::Enum(item_enum) => { + match Container::new_enum_nested(item_enum, &versions) { + Ok(container) => container, + Err(err) => return err.write_errors(), + } + } + Item::Struct(item_struct) => { + match Container::new_struct_nested(item_struct, &versions) { + Ok(container) => container, + Err(err) => return err.write_errors(), + } + } + _ => continue, + }; + + containers.push(container); } + + Module::new(module_input, preserve_modules, versions, containers).generate_tokens() + } + Item::Enum(item_enum) => { + let container_attributes: StandaloneContainerAttributes = + match parse_outer_attributes(attrs) { + Ok(ca) => ca, + Err(err) => return err.write_errors(), + }; + + let standalone_enum = + match StandaloneContainer::new_enum(item_enum, container_attributes) { + Ok(standalone_enum) => standalone_enum, + Err(err) => return err.write_errors(), + }; + + standalone_enum.generate_tokens() } Item::Struct(item_struct) => { - let container_attributes = match parse_outer_attributes(attrs) { - Ok(ca) => ca, - Err(err) => return err.write_errors(), - }; + let container_attributes: StandaloneContainerAttributes = + match parse_outer_attributes(attrs) { + Ok(ca) => ca, + Err(err) => return err.write_errors(), + }; - let input = ContainerInput { - original_attributes: item_struct.attrs, - visibility: item_struct.vis, - ident: item_struct.ident, - }; + let standalone_struct = + match StandaloneContainer::new_struct(item_struct, container_attributes) { + Ok(standalone_struct) => standalone_struct, + Err(err) => return err.write_errors(), + }; - match VersionedStruct::new(input, item_struct.fields, container_attributes) { - Ok(versioned_struct) => versioned_struct.generate_standalone_tokens(), - Err(err) => Error::into_compile_error(err), - } + standalone_struct.generate_tokens() } _ => Error::new( input.span(), diff --git a/crates/stackable-versioned-macros/src/utils.rs b/crates/stackable-versioned-macros/src/utils.rs new file mode 100644 index 000000000..d2003c7b2 --- /dev/null +++ b/crates/stackable-versioned-macros/src/utils.rs @@ -0,0 +1,117 @@ +use std::ops::Deref; + +use convert_case::{Case, Casing}; +use darling::util::IdentString; +use k8s_version::Version; +use quote::{format_ident, ToTokens}; +use syn::{spanned::Spanned, Ident}; + +pub(crate) trait VersionExt { + fn as_variant_ident(&self) -> IdentString; +} + +impl VersionExt for Version { + fn as_variant_ident(&self) -> IdentString { + format_ident!("{ident}", ident = self.to_string().to_case(Case::Pascal)).into() + } +} + +/// Provides extra functionality on top of [`IdentString`]s used to name containers. +pub(crate) trait ContainerIdentExt { + /// Removes the 'Spec' suffix from the [`IdentString`]. + fn as_cleaned_kubernetes_ident(&self) -> IdentString; + + /// Transforms the [`IdentString`] into one usable in the [`From`] impl. + fn as_from_impl_ident(&self) -> IdentString; +} + +impl ContainerIdentExt for Ident { + fn as_cleaned_kubernetes_ident(&self) -> IdentString { + let ident = format_ident!("{}", self.to_string().trim_end_matches("Spec")); + IdentString::new(ident) + } + + fn as_from_impl_ident(&self) -> IdentString { + let ident = format_ident!("__sv_{}", self.to_string().to_lowercase()); + IdentString::new(ident) + } +} + +pub(crate) trait ItemIdentExt: Deref + From + Spanned { + const DEPRECATED_PREFIX: &'static str; + + fn deprecated_prefix(&self) -> &'static str { + Self::DEPRECATED_PREFIX + } + + fn starts_with_deprecated_prefix(&self) -> bool { + self.deref().as_str().starts_with(Self::DEPRECATED_PREFIX) + } + + /// Removes deprecation prefixed from field or variant idents. + fn as_cleaned_ident(&self) -> IdentString; +} + +pub(crate) struct FieldIdent(IdentString); + +impl ItemIdentExt for FieldIdent { + const DEPRECATED_PREFIX: &'static str = "deprecated_"; + + fn as_cleaned_ident(&self) -> IdentString { + self.0 + .clone() + .map(|i| i.trim_start_matches(Self::DEPRECATED_PREFIX).to_string()) + } +} + +impl From for FieldIdent { + fn from(value: Ident) -> Self { + Self(IdentString::from(value)) + } +} + +impl Deref for FieldIdent { + type Target = IdentString; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ToTokens for FieldIdent { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.0.to_tokens(tokens); + } +} + +pub(crate) struct VariantIdent(IdentString); + +impl ItemIdentExt for VariantIdent { + const DEPRECATED_PREFIX: &'static str = "Deprecated"; + + fn as_cleaned_ident(&self) -> IdentString { + self.0 + .clone() + .map(|i| i.trim_start_matches(Self::DEPRECATED_PREFIX).to_string()) + } +} + +impl From for VariantIdent { + fn from(value: Ident) -> Self { + Self(IdentString::from(value)) + } +} + +impl Deref for VariantIdent { + type Target = IdentString; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ToTokens for VariantIdent { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.0.to_tokens(tokens); + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 1de01fa45..2e2b8c852 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.81.0" +channel = "1.82.0" From cd125d25f17db3bc127c6e95b68257cf229d1c96 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 14 Nov 2024 09:44:31 +0100 Subject: [PATCH 07/33] fix: Correctly emit automatically_derived and deprecated attributes --- .../src/attrs/common.rs | 4 ++-- .../src/codegen/container/enum.rs | 4 ++-- .../src/codegen/container/mod.rs | 15 +++++++++++--- .../src/codegen/container/struct.rs | 15 ++++++-------- .../src/codegen/mod.rs | 16 ++++++++++++--- .../src/codegen/module.rs | 20 ++++++++++++++++++- 6 files changed, 54 insertions(+), 20 deletions(-) diff --git a/crates/stackable-versioned-macros/src/attrs/common.rs b/crates/stackable-versioned-macros/src/attrs/common.rs index c243f0d1a..67f6552f5 100644 --- a/crates/stackable-versioned-macros/src/attrs/common.rs +++ b/crates/stackable-versioned-macros/src/attrs/common.rs @@ -1,5 +1,5 @@ use darling::{ - util::{Flag, SpannedValue}, + util::{Flag, Override, SpannedValue}, Error, FromMeta, Result, }; use itertools::Itertools; @@ -75,7 +75,7 @@ pub(crate) struct RootOptions { /// - `doc` option to add version-specific documentation. #[derive(Clone, Debug, FromMeta)] pub(crate) struct VersionArguments { - pub(crate) deprecated: Flag, + pub(crate) deprecated: Option>, pub(crate) name: Version, pub(crate) skip: Option, pub(crate) doc: Option, diff --git a/crates/stackable-versioned-macros/src/codegen/container/enum.rs b/crates/stackable-versioned-macros/src/codegen/container/enum.rs index ff251695d..b6f8ea930 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/enum.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/enum.rs @@ -139,8 +139,8 @@ impl Enum { // 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 - || next_version.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)] }); diff --git a/crates/stackable-versioned-macros/src/codegen/container/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/mod.rs index 7907544f4..e90e57bd6 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/mod.rs @@ -43,11 +43,11 @@ impl Container { &self, version: &VersionDefinition, next_version: Option<&VersionDefinition>, - is_nested: bool, + add_attributes: bool, ) -> Option { match self { - Container::Struct(s) => s.generate_from_impl(version, next_version, is_nested), - Container::Enum(e) => e.generate_from_impl(version, next_version, is_nested), + Container::Struct(s) => s.generate_from_impl(version, next_version, add_attributes), + Container::Enum(e) => e.generate_from_impl(version, next_version, add_attributes), } } @@ -135,6 +135,12 @@ impl StandaloneContainer { self.container .generate_from_impl(version, versions.peek().copied(), false); + // Add the #[deprecated] attribute when the version is marked as deprecated. + let deprecated_attribute = version + .deprecated + .as_ref() + .map(|note| quote! { #[deprecated = #note] }); + // Generate Kubernetes specific code which is placed outside of the container // definition. if let Some((enum_variant, fn_call)) = self.container.generate_kubernetes_item(version) @@ -146,7 +152,10 @@ impl StandaloneContainer { let version_ident = &version.ident; tokens.extend(quote! { + #[automatically_derived] + #deprecated_attribute #vis mod #version_ident { + use super::*; #container_definition } diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct.rs b/crates/stackable-versioned-macros/src/codegen/container/struct.rs index 1e83c58d7..cbf421590 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct.rs @@ -103,10 +103,6 @@ impl Container { common, })) } - - fn new_struct() -> Result { - todo!() - } } pub(crate) struct Struct { @@ -143,7 +139,7 @@ impl Struct { &self, version: &VersionDefinition, next_version: Option<&VersionDefinition>, - is_nested: bool, + add_attributes: bool, ) -> Option { if version.skip_from || self.common.options.skip_from { return None; @@ -162,16 +158,17 @@ impl Struct { // Include allow(deprecated) only when this or the next version is // deprecated. Also include it, when a field in this or the next // version is deprecated. - let allow_attribute = (version.deprecated - || next_version.deprecated + let allow_attribute = (version.deprecated.is_some() + || next_version.deprecated.is_some() || self.is_any_field_deprecated(version) || self.is_any_field_deprecated(next_version)) .then(|| 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 = - is_nested.not().then(|| quote! {#[automatically_derived]}); + let automatically_derived = add_attributes + .not() + .then(|| quote! {#[automatically_derived]}); Some(quote! { #automatically_derived diff --git a/crates/stackable-versioned-macros/src/codegen/mod.rs b/crates/stackable-versioned-macros/src/codegen/mod.rs index 87a975a9f..28c3fe70a 100644 --- a/crates/stackable-versioned-macros/src/codegen/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/mod.rs @@ -13,7 +13,7 @@ pub(crate) mod module; #[derive(Debug)] pub(crate) struct VersionDefinition { /// Indicates that the container version is deprecated. - pub(crate) deprecated: bool, + pub(crate) deprecated: Option, /// Indicates that the generation of `From for NEW` should be skipped. pub(crate) skip_from: bool, @@ -38,7 +38,12 @@ impl From<&StandaloneContainerAttributes> for Vec { .map(|v| VersionDefinition { skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()), ident: format_ident!("{version}", version = v.name.to_string()).into(), - deprecated: v.deprecated.is_present(), + deprecated: v.deprecated.as_ref().map(|r#override| { + r#override.clone().unwrap_or(format!( + "Version {version} is deprecated", + version = v.name.to_string() + )) + }), docs: process_docs(&v.doc), inner: v.name, }) @@ -55,7 +60,12 @@ impl From<&ModuleAttributes> for Vec { .map(|v| VersionDefinition { skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()), ident: format_ident!("{version}", version = v.name.to_string()).into(), - deprecated: v.deprecated.is_present(), + deprecated: v.deprecated.as_ref().map(|r#override| { + r#override.clone().unwrap_or(format!( + "Version {version} is deprecated", + version = v.name.to_string() + )) + }), docs: process_docs(&v.doc), inner: v.name, }) diff --git a/crates/stackable-versioned-macros/src/codegen/module.rs b/crates/stackable-versioned-macros/src/codegen/module.rs index 96f577d9f..b71934fcb 100644 --- a/crates/stackable-versioned-macros/src/codegen/module.rs +++ b/crates/stackable-versioned-macros/src/codegen/module.rs @@ -1,3 +1,5 @@ +use std::ops::Not; + use darling::util::IdentString; use proc_macro2::TokenStream; use quote::quote; @@ -64,11 +66,26 @@ impl Module { from_impls.extend(container.generate_from_impl( version, versions.peek().copied(), - true, + self.preserve_module, )); } + // Only add #[automatically_derived] here if the user doesn't want to preserve the + // module. + let automatically_derived = self + .preserve_module + .not() + .then(|| quote! {#[automatically_derived]}); + + // Add the #[deprecated] attribute when the version is marked as deprecated. + let deprecated_attribute = version + .deprecated + .as_ref() + .map(|note| quote! { #[deprecated = #note] }); + tokens.extend(quote! { + #automatically_derived + #deprecated_attribute #version_module_vis mod #version_ident { use super::*; @@ -81,6 +98,7 @@ impl Module { if self.preserve_module { quote! { + #[automatically_derived] #module_vis mod #module_ident { #tokens } From 488b409957ab89a9104e219b23a435c0a2d6614d Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 14 Nov 2024 09:44:59 +0100 Subject: [PATCH 08/33] fix: Skip From impls for enum if options are set --- .../stackable-versioned-macros/src/codegen/container/enum.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/stackable-versioned-macros/src/codegen/container/enum.rs b/crates/stackable-versioned-macros/src/codegen/container/enum.rs index b6f8ea930..6a9c7c80a 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/enum.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/enum.rs @@ -119,6 +119,10 @@ impl Enum { next_version: Option<&VersionDefinition>, is_nested: bool, ) -> Option { + if version.skip_from || self.common.options.skip_from { + return None; + } + match next_version { Some(next_version) => { let enum_ident = &self.common.idents.original; From 35d01426e989e309fb826ca21ce89683e96fc911 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 14 Nov 2024 09:46:29 +0100 Subject: [PATCH 09/33] fix: Correctly emit doc commens --- ...ioned_macros__test__default_snapshots@attribute_enum.rs.snap | 1 - ...ned_macros__test__default_snapshots@attribute_struct.rs.snap | 1 - crates/stackable-versioned-macros/src/codegen/container/enum.rs | 2 ++ .../stackable-versioned-macros/src/codegen/container/struct.rs | 2 ++ 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_enum.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_enum.rs.snap index 32f2162a9..22a6eaa73 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_enum.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_enum.rs.snap @@ -21,7 +21,6 @@ mod v1alpha1 { #[automatically_derived] mod v1beta1 { use super::*; - /// ///Additional docs for this version which are purposefully long to ///show how manual line wrapping works. \ ///Multi-line docs are also supported, as per regular doc-comments. diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_struct.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_struct.rs.snap index 440311ca7..05b0bfd21 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_struct.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_struct.rs.snap @@ -20,7 +20,6 @@ mod v1alpha1 { #[automatically_derived] mod v1beta1 { use super::*; - /// ///Additional docs for this version which are purposefully long to ///show how manual line wrapping works. \ ///Multi-line docs are also supported, as per regular doc-comments. diff --git a/crates/stackable-versioned-macros/src/codegen/container/enum.rs b/crates/stackable-versioned-macros/src/codegen/container/enum.rs index 6a9c7c80a..eb95a5292 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/enum.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/enum.rs @@ -99,6 +99,7 @@ impl Enum { pub(crate) fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { let original_attributes = &self.common.original_attributes; let ident = &self.common.idents.original; + let version_docs = &version.docs; let mut variants = TokenStream::new(); for variant in &self.variants { @@ -106,6 +107,7 @@ impl Enum { } quote! { + #(#[doc = #version_docs])* #(#original_attributes)* pub enum #ident { #variants diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct.rs b/crates/stackable-versioned-macros/src/codegen/container/struct.rs index cbf421590..20a87ac1a 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct.rs @@ -117,6 +117,7 @@ impl Struct { pub(crate) fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { let original_attributes = &self.common.original_attributes; let ident = &self.common.idents.original; + let version_docs = &version.docs; let mut fields = TokenStream::new(); for field in &self.fields { @@ -127,6 +128,7 @@ impl Struct { let kubernetes_cr_derive = self.generate_kubernetes_cr_derive(version); quote! { + #(#[doc = #version_docs])* #(#original_attributes)* #kubernetes_cr_derive pub struct #ident { From 0e4a7048f53e1dad3a267ac0665261e8d29de54d Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 14 Nov 2024 09:47:27 +0100 Subject: [PATCH 10/33] fix: Correctly emit merged_crd code --- .../src/codegen/container/mod.rs | 26 ++++++++++++------- .../src/codegen/container/struct.rs | 14 +++++----- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/crates/stackable-versioned-macros/src/codegen/container/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/mod.rs index e90e57bd6..518ea1ac4 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/mod.rs @@ -54,7 +54,7 @@ impl Container { pub(crate) fn generate_kubernetes_item( &self, version: &VersionDefinition, - ) -> Option<(IdentString, TokenStream)> { + ) -> Option<(IdentString, String, TokenStream)> { match self { Container::Struct(s) => s.generate_kubernetes_item(version), Container::Enum(_) => None, @@ -63,14 +63,18 @@ impl Container { pub(crate) fn generate_kubernetes_merge_crds( &self, - enum_variants: Vec, + enum_variant_idents: Vec, + enum_variant_strings: Vec, fn_calls: Vec, is_nested: bool, ) -> Option { match self { - Container::Struct(s) => { - s.generate_kubernetes_merge_crds(enum_variants, fn_calls, is_nested) - } + Container::Struct(s) => s.generate_kubernetes_merge_crds( + enum_variant_idents, + enum_variant_strings, + fn_calls, + is_nested, + ), Container::Enum(_) => None, } } @@ -122,7 +126,8 @@ impl StandaloneContainer { let mut tokens = TokenStream::new(); let mut kubernetes_merge_crds_fn_calls = Vec::new(); - let mut kubernetes_enum_variants = Vec::new(); + let mut kubernetes_enum_variant_idents = Vec::new(); + let mut kubernetes_enum_variant_strings = Vec::new(); let mut versions = self.versions.iter().peekable(); @@ -143,10 +148,12 @@ impl StandaloneContainer { // Generate Kubernetes specific code which is placed outside of the container // definition. - if let Some((enum_variant, fn_call)) = self.container.generate_kubernetes_item(version) + if let Some((enum_variant_ident, enum_variant_string, fn_call)) = + self.container.generate_kubernetes_item(version) { kubernetes_merge_crds_fn_calls.push(fn_call); - kubernetes_enum_variants.push(enum_variant); + kubernetes_enum_variant_idents.push(enum_variant_ident); + kubernetes_enum_variant_strings.push(enum_variant_string); } let version_ident = &version.ident; @@ -164,7 +171,8 @@ impl StandaloneContainer { } tokens.extend(self.container.generate_kubernetes_merge_crds( - kubernetes_enum_variants, + kubernetes_enum_variant_idents, + kubernetes_enum_variant_strings, kubernetes_merge_crds_fn_calls, false, )); diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct.rs b/crates/stackable-versioned-macros/src/codegen/container/struct.rs index 20a87ac1a..16bdfbfab 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct.rs @@ -264,10 +264,11 @@ impl Struct { pub(crate) fn generate_kubernetes_item( &self, version: &VersionDefinition, - ) -> Option<(IdentString, TokenStream)> { + ) -> Option<(IdentString, String, TokenStream)> { match &self.common.options.kubernetes_options { Some(_) => { let enum_variant_ident = version.inner.as_variant_ident(); + let enum_variant_string = version.inner.to_string(); let struct_ident = &self.common.idents.kubernetes; let module_ident = &version.ident; @@ -277,7 +278,7 @@ impl Struct { <#qualified_path as ::kube::CustomResourceExt>::crd() }; - Some((enum_variant_ident, merge_crds_fn_call)) + Some((enum_variant_ident, enum_variant_string, merge_crds_fn_call)) } None => None, } @@ -285,11 +286,12 @@ impl Struct { pub(crate) fn generate_kubernetes_merge_crds( &self, - enum_variants: Vec, + enum_variant_idents: Vec, + enum_variant_strings: Vec, fn_calls: Vec, is_nested: bool, ) -> Option { - if enum_variants.is_empty() { + if enum_variant_idents.is_empty() { return None; } @@ -303,14 +305,14 @@ impl Struct { Some(quote! { #automatically_derived pub enum #enum_ident { - #(#enum_variants),* + #(#enum_variant_idents),* } #automatically_derived impl ::std::fmt::Display for #enum_ident { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { match self { - #(Self::#enum_variants => f.write_str("stuff")),* + #(Self::#enum_variant_idents => f.write_str(#enum_variant_strings)),* } } } From be3a7a16ee9d88e2062db86c33fcd6dd18157a68 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 14 Nov 2024 10:06:04 +0100 Subject: [PATCH 11/33] fix: Correctly skip merged_crd generation This also adds a test to validate that the relevant code is not generated of the skip option is set. --- .../fixtures/inputs/k8s/skip.rs | 22 ++++++ ...d_macros__test__k8s_snapshots@skip.rs.snap | 76 +++++++++++++++++++ .../src/codegen/container/struct.rs | 4 +- 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 crates/stackable-versioned-macros/fixtures/inputs/k8s/skip.rs create mode 100644 crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@skip.rs.snap diff --git a/crates/stackable-versioned-macros/fixtures/inputs/k8s/skip.rs b/crates/stackable-versioned-macros/fixtures/inputs/k8s/skip.rs new file mode 100644 index 000000000..aa9f908fa --- /dev/null +++ b/crates/stackable-versioned-macros/fixtures/inputs/k8s/skip.rs @@ -0,0 +1,22 @@ +#[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + version(name = "v1"), + k8s( + group = "stackable.tech", + singular = "foo", + plural = "foos", + namespaced, + skip(merged_crd) + ) +)] +// --- +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +pub struct FooSpec { + #[versioned( + added(since = "v1beta1"), + changed(since = "v1", from_name = "bah", from_type = "u16") + )] + bar: usize, + baz: bool, +} diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@skip.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@skip.rs.snap new file mode 100644 index 000000000..1b81fbf41 --- /dev/null +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@skip.rs.snap @@ -0,0 +1,76 @@ +--- +source: crates/stackable-versioned-macros/src/lib.rs +expression: formatted +input_file: crates/stackable-versioned-macros/fixtures/inputs/k8s/skip.rs +--- +#[automatically_derived] +pub mod v1alpha1 { + use super::*; + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] + #[derive(::kube::CustomResource)] + #[kube( + group = "stackable.tech", + version = "v1alpha1", + kind = "Foo", + singular = "foo", + plural = "foos", + namespaced + )] + pub struct FooSpec { + pub baz: bool, + } +} +#[automatically_derived] +impl ::std::convert::From for v1beta1::FooSpec { + fn from(__sv_foospec: v1alpha1::FooSpec) -> Self { + Self { + bah: ::std::default::Default::default(), + baz: __sv_foospec.baz, + } + } +} +#[automatically_derived] +pub mod v1beta1 { + use super::*; + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] + #[derive(::kube::CustomResource)] + #[kube( + group = "stackable.tech", + version = "v1beta1", + kind = "Foo", + singular = "foo", + plural = "foos", + namespaced + )] + pub struct FooSpec { + pub bah: u16, + pub baz: bool, + } +} +#[automatically_derived] +impl ::std::convert::From for v1::FooSpec { + fn from(__sv_foospec: v1beta1::FooSpec) -> Self { + Self { + bar: __sv_foospec.bah.into(), + baz: __sv_foospec.baz, + } + } +} +#[automatically_derived] +pub mod v1 { + use super::*; + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] + #[derive(::kube::CustomResource)] + #[kube( + group = "stackable.tech", + version = "v1", + kind = "Foo", + singular = "foo", + plural = "foos", + namespaced + )] + pub struct FooSpec { + pub bar: usize, + pub baz: bool, + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct.rs b/crates/stackable-versioned-macros/src/codegen/container/struct.rs index 16bdfbfab..f91933ab1 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct.rs @@ -266,7 +266,7 @@ impl Struct { version: &VersionDefinition, ) -> Option<(IdentString, String, TokenStream)> { match &self.common.options.kubernetes_options { - Some(_) => { + Some(options) if !options.skip_merged_crd => { let enum_variant_ident = version.inner.as_variant_ident(); let enum_variant_string = version.inner.to_string(); @@ -280,7 +280,7 @@ impl Struct { Some((enum_variant_ident, enum_variant_string, merge_crds_fn_call)) } - None => None, + _ => None, } } From 13b2a5e27ebcc16fc319c2e15b355e0c8f7bcc4a Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 14 Nov 2024 10:07:27 +0100 Subject: [PATCH 12/33] chore: Fix clippy errors --- .../src/codegen/mod.rs | 34 ++++--------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/crates/stackable-versioned-macros/src/codegen/mod.rs b/crates/stackable-versioned-macros/src/codegen/mod.rs index 28c3fe70a..8b2c8dba9 100644 --- a/crates/stackable-versioned-macros/src/codegen/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/mod.rs @@ -39,10 +39,9 @@ impl From<&StandaloneContainerAttributes> for Vec { skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()), ident: format_ident!("{version}", version = v.name.to_string()).into(), deprecated: v.deprecated.as_ref().map(|r#override| { - r#override.clone().unwrap_or(format!( - "Version {version} is deprecated", - version = v.name.to_string() - )) + r#override + .clone() + .unwrap_or(format!("Version {version} is deprecated", version = v.name)) }), docs: process_docs(&v.doc), inner: v.name, @@ -61,10 +60,9 @@ impl From<&ModuleAttributes> for Vec { skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()), ident: format_ident!("{version}", version = v.name.to_string()).into(), deprecated: v.deprecated.as_ref().map(|r#override| { - r#override.clone().unwrap_or(format!( - "Version {version} is deprecated", - version = v.name.to_string() - )) + r#override + .clone() + .unwrap_or(format!("Version {version} is deprecated", version = v.name)) }), docs: process_docs(&v.doc), inner: v.name, @@ -113,26 +111,6 @@ impl ItemStatus { } } -pub(crate) struct Change { - pub(crate) item_ident: IdentString, - pub(crate) item_type: Type, - pub(crate) ty: ChangeType, -} - -pub(crate) enum ChangeType { - Added { - default_fn: Path, - }, - Changed { - from_ident: IdentString, - from_type: Type, - }, - Deprecated { - from_ident: IdentString, - note: Option, - }, -} - /// Converts lines of doc-comments into a trimmed list. fn process_docs(input: &Option) -> Vec { if let Some(input) = input { From 56d5ce23b2a06250b6f5c4e1bc019eb1743275fd Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 14 Nov 2024 16:22:48 +0100 Subject: [PATCH 13/33] test: Add module snapshot tests --- .../fixtures/inputs/default/module.rs | 22 ++++++ .../inputs/default/module_preserve.rs | 23 ++++++ ...os__test__default_snapshots@module.rs.snap | 74 +++++++++++++++++++ ..._default_snapshots@module_preserve.rs.snap | 70 ++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 crates/stackable-versioned-macros/fixtures/inputs/default/module.rs create mode 100644 crates/stackable-versioned-macros/fixtures/inputs/default/module_preserve.rs create mode 100644 crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@module.rs.snap create mode 100644 crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@module_preserve.rs.snap diff --git a/crates/stackable-versioned-macros/fixtures/inputs/default/module.rs b/crates/stackable-versioned-macros/fixtures/inputs/default/module.rs new file mode 100644 index 000000000..003f7d243 --- /dev/null +++ b/crates/stackable-versioned-macros/fixtures/inputs/default/module.rs @@ -0,0 +1,22 @@ +#[versioned( + version(name = "v1alpha1"), + version(name = "v1"), + version(name = "v2alpha1") +)] +// --- +pub(crate) mod versioned { + pub struct Foo { + bar: usize, + + #[versioned(added(since = "v1"))] + baz: bool, + + #[versioned(deprecated(since = "v2alpha1"))] + deprecated_foo: String, + } + + #[versioned] + pub struct Bar { + baz: String, + } +} diff --git a/crates/stackable-versioned-macros/fixtures/inputs/default/module_preserve.rs b/crates/stackable-versioned-macros/fixtures/inputs/default/module_preserve.rs new file mode 100644 index 000000000..84298e38c --- /dev/null +++ b/crates/stackable-versioned-macros/fixtures/inputs/default/module_preserve.rs @@ -0,0 +1,23 @@ +#[versioned( + version(name = "v1alpha1"), + version(name = "v1"), + version(name = "v2alpha1"), + preserve_module +)] +// --- +pub(crate) mod versioned { + pub struct Foo { + bar: usize, + + #[versioned(added(since = "v1"))] + baz: bool, + + #[versioned(deprecated(since = "v2alpha1"))] + deprecated_foo: String, + } + + #[versioned] + pub struct Bar { + baz: String, + } +} diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@module.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@module.rs.snap new file mode 100644 index 000000000..50a7a308c --- /dev/null +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@module.rs.snap @@ -0,0 +1,74 @@ +--- +source: crates/stackable-versioned-macros/src/lib.rs +expression: formatted +input_file: crates/stackable-versioned-macros/fixtures/inputs/default/module.rs +--- +#[automatically_derived] +pub(crate) mod v1alpha1 { + use super::*; + pub struct Foo { + pub bar: usize, + pub foo: String, + } + pub struct Bar { + pub baz: String, + } +} +#[automatically_derived] +impl ::std::convert::From for v1::Foo { + fn from(__sv_foo: v1alpha1::Foo) -> Self { + Self { + bar: __sv_foo.bar, + baz: ::std::default::Default::default(), + foo: __sv_foo.foo, + } + } +} +#[automatically_derived] +impl ::std::convert::From for v1::Bar { + fn from(__sv_bar: v1alpha1::Bar) -> Self { + Self { baz: __sv_bar.baz } + } +} +#[automatically_derived] +pub(crate) mod v1 { + use super::*; + pub struct Foo { + pub bar: usize, + pub baz: bool, + pub foo: String, + } + pub struct Bar { + pub baz: String, + } +} +#[automatically_derived] +#[allow(deprecated)] +impl ::std::convert::From for v2alpha1::Foo { + fn from(__sv_foo: v1::Foo) -> Self { + Self { + bar: __sv_foo.bar, + baz: __sv_foo.baz, + deprecated_foo: __sv_foo.foo, + } + } +} +#[automatically_derived] +impl ::std::convert::From for v2alpha1::Bar { + fn from(__sv_bar: v1::Bar) -> Self { + Self { baz: __sv_bar.baz } + } +} +#[automatically_derived] +pub(crate) mod v2alpha1 { + use super::*; + pub struct Foo { + pub bar: usize, + pub baz: bool, + #[deprecated] + pub deprecated_foo: String, + } + pub struct Bar { + pub baz: String, + } +} diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@module_preserve.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@module_preserve.rs.snap new file mode 100644 index 000000000..1570369a5 --- /dev/null +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@module_preserve.rs.snap @@ -0,0 +1,70 @@ +--- +source: crates/stackable-versioned-macros/src/lib.rs +expression: formatted +input_file: crates/stackable-versioned-macros/fixtures/inputs/default/module_preserve.rs +--- +#[automatically_derived] +pub(crate) mod versioned { + pub mod v1alpha1 { + use super::*; + pub struct Foo { + pub bar: usize, + pub foo: String, + } + pub struct Bar { + pub baz: String, + } + } + impl ::std::convert::From for v1::Foo { + fn from(__sv_foo: v1alpha1::Foo) -> Self { + Self { + bar: __sv_foo.bar, + baz: ::std::default::Default::default(), + foo: __sv_foo.foo, + } + } + } + impl ::std::convert::From for v1::Bar { + fn from(__sv_bar: v1alpha1::Bar) -> Self { + Self { baz: __sv_bar.baz } + } + } + pub mod v1 { + use super::*; + pub struct Foo { + pub bar: usize, + pub baz: bool, + pub foo: String, + } + pub struct Bar { + pub baz: String, + } + } + #[allow(deprecated)] + impl ::std::convert::From for v2alpha1::Foo { + fn from(__sv_foo: v1::Foo) -> Self { + Self { + bar: __sv_foo.bar, + baz: __sv_foo.baz, + deprecated_foo: __sv_foo.foo, + } + } + } + impl ::std::convert::From for v2alpha1::Bar { + fn from(__sv_bar: v1::Bar) -> Self { + Self { baz: __sv_bar.baz } + } + } + pub mod v2alpha1 { + use super::*; + pub struct Foo { + pub bar: usize, + pub baz: bool, + #[deprecated] + pub deprecated_foo: String, + } + pub struct Bar { + pub baz: String, + } + } +} From 7e233b1b430ab63b13c7d086546eee4cae839417 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 15 Nov 2024 14:34:38 +0100 Subject: [PATCH 14/33] test: Update basic Kubernetes snapshot test --- ...macros__test__k8s_snapshots@basic.rs.snap} | 8 ++-- .../src/codegen/container/struct.rs | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) rename crates/stackable-versioned-macros/fixtures/snapshots/{stackable_versioned_macros__test__k8s_snapshots.snap => stackable_versioned_macros__test__k8s_snapshots@basic.rs.snap} (95%) diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@basic.rs.snap similarity index 95% rename from crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots.snap rename to crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@basic.rs.snap index f75613b5f..694454b2c 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@basic.rs.snap @@ -87,9 +87,9 @@ impl ::std::fmt::Display for Foo { f: &mut ::std::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { match self { - Foo::V1Alpha1 => f.write_str("v1alpha1"), - Foo::V1Beta1 => f.write_str("v1beta1"), - Foo::V1 => f.write_str("v1"), + Self::V1Alpha1 => f.write_str("v1alpha1"), + Self::V1Beta1 => f.write_str("v1beta1"), + Self::V1 => f.write_str("v1"), } } } @@ -106,7 +106,7 @@ impl Foo { vec![ < v1alpha1::Foo as ::kube::CustomResourceExt > ::crd(), < v1beta1::Foo as ::kube::CustomResourceExt > ::crd(), < v1::Foo as - ::kube::CustomResourceExt > ::crd(), + ::kube::CustomResourceExt > ::crd() ], &stored_apiversion.to_string(), ) diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct.rs b/crates/stackable-versioned-macros/src/codegen/container/struct.rs index f91933ab1..78e2b16dc 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct.rs @@ -302,6 +302,9 @@ impl Struct { let automatically_derived = is_nested.not().then(|| quote! {#[automatically_derived]}); // TODO (@Techassi): Use proper visibility instead of hard-coding 'pub' + // TODO (@Techassi): Move the YAML printing code into 'stackable-versioned' so that we don't + // have any cross-dependencies and the macro can be used on it's own (K8s features of course + // still need kube and friends). Some(quote! { #automatically_derived pub enum #enum_ident { @@ -325,6 +328,45 @@ impl Struct { ) -> ::std::result::Result<::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError> { ::kube::core::crd::merge_crds(vec![#(#fn_calls),*], &stored_apiversion.to_string()) } + + /// Generates and writes a merged CRD which contains all versions defined using the `#[versioned()]` + /// macro to a file located at `path`. + pub fn write_merged_crd

(path: P, stored_apiversion: Self, operator_version: &str) -> Result<(), ::stackable_versioned::Error> + where P: AsRef<::std::path::Path> + { + use ::stackable_shared::yaml::{YamlSchema, SerializeOptions}; + + let merged_crd = Self::merged_crd(stored_apiversion).map_err(|err| ::stackable_versioned::Error::MergeCrd { + source: err, + })?; + + YamlSchema::write_yaml_schema( + &merged_crd, + path, + operator_version, + SerializeOptions::default() + ).map_err(|err| ::stackable_versioned::Error::SerializeYaml { + source: err, + }) + } + + /// Generates and writes a merged CRD which contains all versions defined using the `#[versioned()]` + /// macro to stdout. + pub fn print_merged_crd(stored_apiversion: Self, operator_version: &str) -> Result<(), ::stackable_versioned::Error> { + use ::stackable_shared::yaml::{YamlSchema, SerializeOptions}; + + let merged_crd = Self::merged_crd(stored_apiversion).map_err(|err| ::stackable_versioned::Error::MergeCrd { + source: err, + })?; + + YamlSchema::print_yaml_schema( + &merged_crd, + operator_version, + SerializeOptions::default() + ).map_err(|err| ::stackable_versioned::Error::SerializeYaml { + source: err, + }) + } } }) } From f5762dd7ef7cc0eb67b5e9cb884d6c408ccfe180 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 15 Nov 2024 14:54:00 +0100 Subject: [PATCH 15/33] doc: Fix doc comment links --- crates/stackable-versioned-macros/src/attrs/item/field.rs | 6 +++--- crates/stackable-versioned-macros/src/attrs/item/variant.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/stackable-versioned-macros/src/attrs/item/field.rs b/crates/stackable-versioned-macros/src/attrs/item/field.rs index 1d45688b0..bee008fd2 100644 --- a/crates/stackable-versioned-macros/src/attrs/item/field.rs +++ b/crates/stackable-versioned-macros/src/attrs/item/field.rs @@ -9,13 +9,13 @@ use crate::{attrs::item::CommonItemAttributes, codegen::VersionDefinition, utils /// Data stored in this struct is validated using darling's `and_then` attribute. /// During darlings validation, it is not possible to validate that action /// versions match up with declared versions on the container. This validation -/// can be done using the associated [`ValidateVersions::validate_versions`][1] +/// can be done using the associated [`FieldAttributes::validate_versions`][1] /// function. /// /// Rules shared across fields and variants can be found [here][2]. /// -/// [1]: crate::attrs::common::ValidateVersions::validate_versions -/// [2]: crate::attrs::common::ItemAttributes +/// [1]: crate::attrs::item::FieldAttributes::validate_versions +/// [2]: crate::attrs::item::CommonItemAttributes #[derive(Debug, FromField)] #[darling( attributes(versioned), diff --git a/crates/stackable-versioned-macros/src/attrs/item/variant.rs b/crates/stackable-versioned-macros/src/attrs/item/variant.rs index 3cf2cb038..42dc58149 100644 --- a/crates/stackable-versioned-macros/src/attrs/item/variant.rs +++ b/crates/stackable-versioned-macros/src/attrs/item/variant.rs @@ -10,13 +10,13 @@ use crate::{attrs::item::CommonItemAttributes, codegen::VersionDefinition, utils /// Data stored in this struct is validated using darling's `and_then` attribute. /// During darlings validation, it is not possible to validate that action /// versions match up with declared versions on the container. This validation -/// can be done using the associated [`ValidateVersions::validate_versions`][1] +/// can be done using the associated [`VariantAttributes::validate_versions`][1] /// function. /// /// Rules shared across fields and variants can be found [here][2]. /// -/// [1]: crate::attrs::common::ValidateVersions::validate_versions -/// [2]: crate::attrs::common::ItemAttributes +/// [1]: crate::attrs::item::VariantAttributes::validate_versions +/// [2]: crate::attrs::item::CommonItemAttributes #[derive(Debug, FromVariant)] #[darling( attributes(versioned), From ab79ec67caf5ddb58cadf8ebb6d5fa00de62a651 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 15 Nov 2024 14:56:12 +0100 Subject: [PATCH 16/33] chore: Remove unused strum dependency --- Cargo.lock | 1 - crates/stackable-versioned-macros/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2df0bb9ec..bee50eaf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3152,7 +3152,6 @@ dependencies = [ "snafu 0.8.5", "stackable-shared", "stackable-versioned", - "strum", "syn 2.0.82", "trybuild", ] diff --git a/crates/stackable-versioned-macros/Cargo.toml b/crates/stackable-versioned-macros/Cargo.toml index b31ed9487..8f547ae16 100644 --- a/crates/stackable-versioned-macros/Cargo.toml +++ b/crates/stackable-versioned-macros/Cargo.toml @@ -38,7 +38,6 @@ itertools.workspace = true k8s-openapi = { workspace = true, optional = true } kube = { workspace = true, optional = true } proc-macro2.workspace = true -strum.workspace = true syn.workspace = true quote.workspace = true From cb008d370978d4c0b49610d98f4dc16ef7e62818 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 15 Nov 2024 15:00:31 +0100 Subject: [PATCH 17/33] ci(pre-commit): Update rust version and use stackabletech/actions/run-pre-commit --- .github/workflows/pr_pre-commit.yaml | 29 +++------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/.github/workflows/pr_pre-commit.yaml b/.github/workflows/pr_pre-commit.yaml index f1bc0640f..a43bdc6a8 100644 --- a/.github/workflows/pr_pre-commit.yaml +++ b/.github/workflows/pr_pre-commit.yaml @@ -6,7 +6,7 @@ on: env: CARGO_TERM_COLOR: always - RUST_TOOLCHAIN_VERSION: "1.81.0" + RUST_TOOLCHAIN_VERSION: "1.82.0" HADOLINT_VERSION: "v1.17.6" jobs: @@ -16,29 +16,6 @@ jobs: - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 0 - - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + - uses: stackabletech/actions/run-pre-commit@9bd13255f286e4b7a654617268abe1b2f37c3e0a # v0.3.0 with: - python-version: '3.12' - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_TOOLCHAIN_VERSION }} - components: rustfmt,clippy - - name: Setup Hadolint - shell: bash - run: | - set -euo pipefail - - LOCATION_DIR="$HOME/.local/bin" - LOCATION_BIN="$LOCATION_DIR/hadolint" - - SYSTEM=$(uname -s) - ARCH=$(uname -m) - - mkdir -p "$LOCATION_DIR" - curl -sL -o "${LOCATION_BIN}" "https://github.com/hadolint/hadolint/releases/download/${{ env.HADOLINT_VERSION }}/hadolint-$SYSTEM-$ARCH" - chmod 700 "${LOCATION_BIN}" - - echo "$LOCATION_DIR" >> "$GITHUB_PATH" - - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 - with: - extra_args: "--from-ref ${{ github.event.pull_request.base.sha }} --to-ref ${{ github.event.pull_request.head.sha }}" + rust: ${{ env.RUST_TOOLCHAIN_VERSION }} From 7cb3053662bdff8fdafd92f69a461fa9cc18614e Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 15 Nov 2024 15:19:36 +0100 Subject: [PATCH 18/33] fix: Correctly report missing Spec suffix --- .../src/codegen/container/struct.rs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct.rs b/crates/stackable-versioned-macros/src/codegen/container/struct.rs index 78e2b16dc..2d867f709 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct.rs @@ -1,6 +1,6 @@ use std::ops::Not; -use darling::{util::IdentString, FromAttributes, Result}; +use darling::{util::IdentString, Error, FromAttributes, Result}; use proc_macro2::TokenStream; use quote::quote; use syn::{parse_quote, ItemStruct, Path}; @@ -32,6 +32,15 @@ impl Container { } let kubernetes_options = attributes.kubernetes_arguments.map(Into::into); + let idents: ContainerIdents = item_struct.ident.into(); + + // Validate K8s specific requirements + // Ensure that the struct name includes the 'Spec' suffix. + if kubernetes_options.is_some() && !idents.original.as_str().ends_with("Spec") { + return Err(Error::custom( + "struct name needs to include the `Spec` suffix if Kubernetes features are enabled via `#[versioned(k8s())]`" + ).with_span(&idents.original.span())); + } let options = ContainerOptions { skip_from: attributes @@ -42,8 +51,6 @@ impl Container { kubernetes_options, }; - let idents: ContainerIdents = item_struct.ident.into(); - let common = CommonContainerData { original_attributes: item_struct.attrs, options, @@ -71,6 +78,15 @@ impl Container { } let kubernetes_options = attributes.kubernetes_arguments.map(Into::into); + let idents: ContainerIdents = item_struct.ident.into(); + + // Validate K8s specific requirements + // Ensure that the struct name includes the 'Spec' suffix. + if kubernetes_options.is_some() && !idents.original.as_str().ends_with("Spec") { + return Err(Error::custom( + "struct name needs to include the `Spec` suffix if Kubernetes features are enabled via `#[versioned(k8s())]`" + ).with_span(&idents.original.span())); + } let options = ContainerOptions { skip_from: attributes @@ -80,8 +96,6 @@ impl Container { kubernetes_options, }; - let idents: ContainerIdents = item_struct.ident.into(); - // Nested structs // We need to filter out the `versioned` attribute, because these are not directly processed // by darling, but instead by us (using darling). For this reason, darling won't remove the From 7da5697b4e0f0c1be4612e326c3342f58711fc1e Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 15 Nov 2024 15:19:58 +0100 Subject: [PATCH 19/33] test: Update compile-fail test cases --- .../tests/default/fail/changed.stderr | 4 ++-- .../tests/default/fail/deprecate.stderr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/stackable-versioned-macros/tests/default/fail/changed.stderr b/crates/stackable-versioned-macros/tests/default/fail/changed.stderr index a6d0d4070..e4d2e6b16 100644 --- a/crates/stackable-versioned-macros/tests/default/fail/changed.stderr +++ b/crates/stackable-versioned-macros/tests/default/fail/changed.stderr @@ -1,10 +1,10 @@ -error: the previous field name must not start with the deprecation prefix +error: the previous name mustn't start with the deprecation prefix --> tests/default/fail/changed.rs:11:52 | 11 | changed(since = "v1beta1", from_name = "deprecated_bar"), | ^^^^^^^^^^^^^^^^ -error: the previous field name must not start with the deprecation prefix +error: the previous name mustn't start with the deprecation prefix --> tests/default/fail/changed.rs:12:47 | 12 | changed(since = "v1", from_name = "deprecated_baz") diff --git a/crates/stackable-versioned-macros/tests/default/fail/deprecate.stderr b/crates/stackable-versioned-macros/tests/default/fail/deprecate.stderr index c18cca62d..271b0e082 100644 --- a/crates/stackable-versioned-macros/tests/default/fail/deprecate.stderr +++ b/crates/stackable-versioned-macros/tests/default/fail/deprecate.stderr @@ -1,4 +1,4 @@ -error: deprecation must be done using #[versioned(deprecated(since = "VERSION"))] +error: deprecation must be done using `#[versioned(deprecated(since = "VERSION"))]` --> tests/default/fail/deprecate.rs:10:9 | 10 | #[deprecated] From bbf217c4fe1cbd24c9069e0e25f2fffe6483b729 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 15 Nov 2024 15:27:53 +0100 Subject: [PATCH 20/33] chore: Update changelog --- crates/stackable-versioned/CHANGELOG.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/stackable-versioned/CHANGELOG.md b/crates/stackable-versioned/CHANGELOG.md index dc8ead2aa..280a7d795 100644 --- a/crates/stackable-versioned/CHANGELOG.md +++ b/crates/stackable-versioned/CHANGELOG.md @@ -4,19 +4,27 @@ All notable changes to this project will be documented in this file. ## [Unreleased] -## [0.4.1] - 2024-10-23 - ### Added - Add support to apply the `#[versioned()]` macro to modules to version all contained items at once ([#891]). + +### Changed + +- Bump Rust to 1.82.0 ([#891]). + +[#891]: https://github.com/stackabletech/operator-rs/pull/891 + +## [0.4.1] - 2024-10-23 + +### Added + - Add basic handling for enum variants with data ([#892]). ### Fixed - Accept a wider variety of formatting styles in the macro testing regex ([#892]). -[#891]: https://github.com/stackabletech/operator-rs/pull/891 [#892]: https://github.com/stackabletech/operator-rs/pull/892 ## [0.4.0] - 2024-10-14 From 48588551a2105629a9a8dba1f0cfedebae2b0610 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 15 Nov 2024 15:37:22 +0100 Subject: [PATCH 21/33] ci(pre-commit): Add rust-src component --- .github/workflows/pr_pre-commit.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pr_pre-commit.yaml b/.github/workflows/pr_pre-commit.yaml index a43bdc6a8..6f2ceb8e0 100644 --- a/.github/workflows/pr_pre-commit.yaml +++ b/.github/workflows/pr_pre-commit.yaml @@ -19,3 +19,7 @@ jobs: - uses: stackabletech/actions/run-pre-commit@9bd13255f286e4b7a654617268abe1b2f37c3e0a # v0.3.0 with: rust: ${{ env.RUST_TOOLCHAIN_VERSION }} + # rust-src is required for trybuild stderr output comparison to work + # for our cases. + # See: https://github.com/dtolnay/trybuild/issues/236#issuecomment-1620950759 + rust-components: rustfmt,clippy,rust-src From 854f84a6d1a1026599a2ab9c7577bd586d3da934 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 15 Nov 2024 15:40:07 +0100 Subject: [PATCH 22/33] ci(build): Bump Rust version --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a17d34e0c..4ad95fe4c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: '0' CARGO_PROFILE_DEV_DEBUG: '0' - RUST_TOOLCHAIN_VERSION: "1.81.0" + RUST_TOOLCHAIN_VERSION: "1.82.0" RUSTFLAGS: "-D warnings" RUSTDOCFLAGS: "-D warnings" RUST_LOG: "info" From 608c2d6ade6c22c61d7133576aaaa2a90c77b0b9 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 22 Nov 2024 11:54:19 +0100 Subject: [PATCH 23/33] chore: Apply suggestions Co-authored-by: Nick <10092581+NickLarsenNZ@users.noreply.github.com> --- crates/stackable-versioned-macros/src/attrs/common.rs | 4 ++-- .../stackable-versioned-macros/src/codegen/container/enum.rs | 2 +- .../stackable-versioned-macros/src/codegen/container/mod.rs | 1 - .../src/codegen/container/struct.rs | 2 +- crates/stackable-versioned-macros/src/codegen/item/variant.rs | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/stackable-versioned-macros/src/attrs/common.rs b/crates/stackable-versioned-macros/src/attrs/common.rs index 67f6552f5..c70426002 100644 --- a/crates/stackable-versioned-macros/src/attrs/common.rs +++ b/crates/stackable-versioned-macros/src/attrs/common.rs @@ -28,8 +28,8 @@ impl CommonRootArguments { let is_sorted = self.versions.iter().is_sorted_by_key(|v| v.name); - // It needs to be sorted, even tho the definition could be unsorted (if allow_unsorted is - // set). + // It needs to be sorted, even though the definition could be unsorted + // (if allow_unsorted is set). self.versions.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); if !self.options.allow_unsorted.is_present() && !is_sorted { diff --git a/crates/stackable-versioned-macros/src/codegen/container/enum.rs b/crates/stackable-versioned-macros/src/codegen/container/enum.rs index eb95a5292..bf9c735c5 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/enum.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/enum.rs @@ -174,7 +174,7 @@ impl Enum { /// Returns whether any variant is deprecated in the provided `version`. fn is_any_variant_deprecated(&self, version: &VersionDefinition) -> bool { - // First, iterate over all variants. Any will return true if any of the + // First, iterate over all variants. The `any` function will return true if any of the // function invocations return true. If a field doesn't have a chain, // we can safely default to false (unversioned fields cannot be // deprecated). Then we retrieve the status of the field and ensure it diff --git a/crates/stackable-versioned-macros/src/codegen/container/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/mod.rs index 518ea1ac4..1b07e696d 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/mod.rs @@ -91,7 +91,6 @@ impl StandaloneContainer { item_struct: ItemStruct, attributes: StandaloneContainerAttributes, ) -> Result { - // TODO (@Techassi): Only pass the fields we need from item struct instead of moving as a whole let versions: Vec<_> = (&attributes).into(); let vis = item_struct.vis.clone(); diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct.rs b/crates/stackable-versioned-macros/src/codegen/container/struct.rs index 2d867f709..e478d3d73 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct.rs @@ -311,7 +311,7 @@ impl Struct { let enum_ident = &self.common.idents.kubernetes; - // Only add the #[automatically_derived] attribute only if this impl is used outside of a + // Only add the #[automatically_derived] attribute if this impl is used outside of a // module (in standalone mode). let automatically_derived = is_nested.not().then(|| quote! {#[automatically_derived]}); diff --git a/crates/stackable-versioned-macros/src/codegen/item/variant.rs b/crates/stackable-versioned-macros/src/codegen/item/variant.rs index 25352c79f..16ea50729 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/variant.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/variant.rs @@ -65,7 +65,7 @@ impl VersionedVariant { let fields = &self.fields; match &self.changes { - // NOTE (@Techassi): https://rust-lang.github.io/rust-clippy/master/index.html#/expect_fun_call + // NOTE (@Techassi): `unwrap_or_else` used instead of `expect`. See: https://rust-lang.github.io/rust-clippy/master/index.html#/expect_fun_call Some(changes) => match changes.get(&version.inner).unwrap_or_else(|| { panic!( "internal error: chain must contain container version {}", From 626915a4ebac7e6ab922e1810e9a2e55f5e55389 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 22 Nov 2024 11:45:46 +0100 Subject: [PATCH 24/33] chore(test): Add comment about empty versioned attribute --- .../stackable-versioned-macros/fixtures/inputs/default/module.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/stackable-versioned-macros/fixtures/inputs/default/module.rs b/crates/stackable-versioned-macros/fixtures/inputs/default/module.rs index 003f7d243..e16336ed7 100644 --- a/crates/stackable-versioned-macros/fixtures/inputs/default/module.rs +++ b/crates/stackable-versioned-macros/fixtures/inputs/default/module.rs @@ -15,6 +15,7 @@ pub(crate) mod versioned { deprecated_foo: String, } + // The following attribute is just to ensure no strange behavior occurs. #[versioned] pub struct Bar { baz: String, From e2eed778e255247620a5805a27fd394a06b9bc19 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 22 Nov 2024 13:02:36 +0100 Subject: [PATCH 25/33] chore: Add missing doc comments for supported K8s arguments --- crates/stackable-versioned-macros/src/attrs/k8s.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/stackable-versioned-macros/src/attrs/k8s.rs b/crates/stackable-versioned-macros/src/attrs/k8s.rs index 88e1ed051..085136925 100644 --- a/crates/stackable-versioned-macros/src/attrs/k8s.rs +++ b/crates/stackable-versioned-macros/src/attrs/k8s.rs @@ -2,11 +2,17 @@ use darling::{util::Flag, FromMeta}; /// This struct contains supported Kubernetes arguments. /// +/// The arguments are passed through to the `#[kube]` attribute. More details can be found in the +/// official docs: . +/// /// Supported arguments are: /// /// - `skip`, which controls skipping parts of the generation. +/// - `singular`, to specify the singular name of the CR object. +/// - `plural`, to specify the plural name of the CR object. /// - `kind`, which allows overwriting the kind field of the CRD. This defaults to the struct name /// (without the 'Spec' suffix). +/// - `namespaced`, to specify that this is a namespaced resource rather than cluster level. /// - `group`, which sets the CRD group, usually the domain of the company. #[derive(Clone, Debug, FromMeta)] pub(crate) struct KubernetesArguments { From 4e346627733e84e087f5b6115498b93cc08665c4 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 22 Nov 2024 13:06:35 +0100 Subject: [PATCH 26/33] chore: Replace unwrap with expect --- .../stackable-versioned-macros/src/codegen/item/field.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/stackable-versioned-macros/src/codegen/item/field.rs b/crates/stackable-versioned-macros/src/codegen/item/field.rs index 951ce774b..77a555038 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/field.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/field.rs @@ -24,11 +24,14 @@ pub(crate) struct VersionedField { impl VersionedField { pub(crate) fn new(field: Field, versions: &[VersionDefinition]) -> Result { - // TODO (@Techassi): Remove unwrap let field_attributes = FieldAttributes::from_field(&field)?; field_attributes.validate_versions(versions)?; - let field_ident = FieldIdent::from(field.ident.unwrap()); + let field_ident = FieldIdent::from( + field + .ident + .expect("internal error: field must have an ident"), + ); let changes = field_attributes .common .into_changeset(&field_ident, field.ty.clone()); From 9147d0b2c8ffd900a638e61ccf62a702f62f232d Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 22 Nov 2024 13:12:29 +0100 Subject: [PATCH 27/33] chore: Adjust FIXME comment --- .../src/codegen/item/variant.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/stackable-versioned-macros/src/codegen/item/variant.rs b/crates/stackable-versioned-macros/src/codegen/item/variant.rs index 16ea50729..db42da520 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/variant.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/variant.rs @@ -29,9 +29,9 @@ impl VersionedVariant { let variant_ident = VariantIdent::from(variant.ident); - // FIXME (@Techassi): As we currently don't support enum variants with - // data, we just return the Never type as the code generation code for - // enum variants won't use this type information. + // FIXME (@Techassi): The chain of changes currently doesn't track versioning of variant + // date and as such, we just use the never type here. During codegen, we just re-emit the + // variant data as is. let ty = Type::Never(TypeNever { bang_token: Not([Span::call_site()]), }); @@ -65,7 +65,8 @@ impl VersionedVariant { let fields = &self.fields; match &self.changes { - // NOTE (@Techassi): `unwrap_or_else` used instead of `expect`. See: https://rust-lang.github.io/rust-clippy/master/index.html#/expect_fun_call + // NOTE (@Techassi): `unwrap_or_else` used instead of `expect`. + // See: https://rust-lang.github.io/rust-clippy/master/index.html#/expect_fun_call Some(changes) => match changes.get(&version.inner).unwrap_or_else(|| { panic!( "internal error: chain must contain container version {}", From 5854505348f458f944f89cdd51a845c6f446ed1c Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 22 Nov 2024 13:17:40 +0100 Subject: [PATCH 28/33] chore: Add explanation comment --- crates/stackable-versioned-macros/src/codegen/module.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/stackable-versioned-macros/src/codegen/module.rs b/crates/stackable-versioned-macros/src/codegen/module.rs index b71934fcb..358f59cf1 100644 --- a/crates/stackable-versioned-macros/src/codegen/module.rs +++ b/crates/stackable-versioned-macros/src/codegen/module.rs @@ -41,7 +41,10 @@ impl Module { return quote! {}; } - // TODO (@Techassi): Leave comment explaining this + // If the 'preserve_module' flag is provided by the user, we need to change the visibility + // of version modules (eg. 'v1alpha1') to be public, so that they are accessible inside the + // preserved (wrapping) module. Otherwise, we can inherit the visibility from the module + // which will be erased. let version_module_vis = if self.preserve_module { &Visibility::Public(Pub::default()) } else { From e9f72bd97053449afcb53b7b733eea2e186e72bd Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 22 Nov 2024 13:18:46 +0100 Subject: [PATCH 29/33] chore: Remove outdated comments --- crates/stackable-versioned-macros/src/lib.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs index b3730bd24..8e2750d09 100644 --- a/crates/stackable-versioned-macros/src/lib.rs +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -483,26 +483,11 @@ println!("{}", serde_yaml::to_string(&merged_crd).unwrap()); /// a cluster scoped. #[proc_macro_attribute] pub fn versioned(attrs: TokenStream, input: TokenStream) -> TokenStream { - // NOTE (@Techassi): For now, we can just use the DeriveInput type here, - // because we only support structs end enums to be versioned. - // In the future - if we decide to support modules - this requires - // adjustments to also support modules. One possible solution might be to - // use an enum with two variants: Container(DeriveInput) and - // Module(ItemMod). let input = syn::parse_macro_input!(input as Item); versioned_impl(attrs.into(), input).into() } fn versioned_impl(attrs: proc_macro2::TokenStream, input: Item) -> proc_macro2::TokenStream { - // NOTE (@Techassi): This derive macro cannot handle multiple structs / enums - // to be versioned within the same file. This is because we cannot declare - // modules more than once (They will not be merged, like impl blocks for - // example). This leads to collisions if there are multiple structs / enums - // which declare the same version. This could maybe be solved by using an - // attribute macro applied to a module with all struct / enums declared in said - // module. This would allow us to generate all versioned structs and enums in - // a single sweep and put them into the appropriate module. - // TODO (@Techassi): Think about how we can handle nested structs / enums which // are also versioned. From f2670202edfcc50dc4d15a01d9ecc0f788492e05 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 22 Nov 2024 13:20:28 +0100 Subject: [PATCH 30/33] chore: Add doc comment to validate_added_action function --- crates/stackable-versioned-macros/src/attrs/item/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/stackable-versioned-macros/src/attrs/item/mod.rs b/crates/stackable-versioned-macros/src/attrs/item/mod.rs index 7b2b59256..61759cf19 100644 --- a/crates/stackable-versioned-macros/src/attrs/item/mod.rs +++ b/crates/stackable-versioned-macros/src/attrs/item/mod.rs @@ -204,6 +204,9 @@ impl CommonItemAttributes { Ok(()) } + /// This associated function is called by the top-level validation function + /// and validates that parameters provided to the `added` actions are + /// valid. fn validate_added_action(&self) -> Result<()> { // NOTE (@Techassi): Can the path actually be empty? if let Some(added) = &self.added { From 14b868fd18d1e138d74ba012f247e3ef9d4e8dd8 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 22 Nov 2024 13:22:11 +0100 Subject: [PATCH 31/33] chore: Replace mustn't with must not in error message --- crates/stackable-versioned-macros/src/attrs/item/mod.rs | 4 ++-- .../tests/default/fail/changed.stderr | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/stackable-versioned-macros/src/attrs/item/mod.rs b/crates/stackable-versioned-macros/src/attrs/item/mod.rs index 61759cf19..712be5043 100644 --- a/crates/stackable-versioned-macros/src/attrs/item/mod.rs +++ b/crates/stackable-versioned-macros/src/attrs/item/mod.rs @@ -197,7 +197,7 @@ impl CommonItemAttributes { if self.deprecated.is_none() && starts_with_deprecated { return Err(Error::custom( - format!("not marked as `deprecated` and thus mustn't include the `{deprecated_prefix}` prefix", deprecated_prefix = item_ident.deprecated_prefix()) + format!("not marked as `deprecated` and thus must not include the `{deprecated_prefix}` prefix", deprecated_prefix = item_ident.deprecated_prefix()) ).with_span(item_ident)); } @@ -231,7 +231,7 @@ impl CommonItemAttributes { if from_name.starts_with(item_ident.deprecated_prefix()) { errors.push( Error::custom( - "the previous name mustn't start with the deprecation prefix", + "the previous name must not start with the deprecation prefix", ) .with_span(&from_name.span()), ); diff --git a/crates/stackable-versioned-macros/tests/default/fail/changed.stderr b/crates/stackable-versioned-macros/tests/default/fail/changed.stderr index e4d2e6b16..9f0b1519e 100644 --- a/crates/stackable-versioned-macros/tests/default/fail/changed.stderr +++ b/crates/stackable-versioned-macros/tests/default/fail/changed.stderr @@ -1,10 +1,10 @@ -error: the previous name mustn't start with the deprecation prefix +error: the previous name must not start with the deprecation prefix --> tests/default/fail/changed.rs:11:52 | 11 | changed(since = "v1beta1", from_name = "deprecated_bar"), | ^^^^^^^^^^^^^^^^ -error: the previous name mustn't start with the deprecation prefix +error: the previous name must not start with the deprecation prefix --> tests/default/fail/changed.rs:12:47 | 12 | changed(since = "v1", from_name = "deprecated_baz") From ad3f4605b28da1dd464a9d89b16a75686e715a21 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 22 Nov 2024 13:32:33 +0100 Subject: [PATCH 32/33] chore: Use concrete type instead of generic type in BTreeMapExt impl --- crates/stackable-versioned-macros/src/codegen/changes.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/stackable-versioned-macros/src/codegen/changes.rs b/crates/stackable-versioned-macros/src/codegen/changes.rs index 80f3849e1..df52a38ca 100644 --- a/crates/stackable-versioned-macros/src/codegen/changes.rs +++ b/crates/stackable-versioned-macros/src/codegen/changes.rs @@ -87,13 +87,10 @@ where fn get_expect(&self, key: &K) -> &V; } -impl BTreeMapExt for BTreeMap -where - K: Ord, -{ +impl BTreeMapExt for BTreeMap { const MESSAGE: &'static str = "internal error: chain must contain version"; - fn get_expect(&self, key: &K) -> &V { + fn get_expect(&self, key: &Version) -> &V { self.get(key).expect(Self::MESSAGE) } } From 63ec18cd8338d10d3eec76b8610315940b0b30af Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 22 Nov 2024 16:31:45 +0100 Subject: [PATCH 33/33] chore: Add doc comments --- .../src/codegen/container/enum.rs | 43 +++++++++++++------ .../src/codegen/container/mod.rs | 31 +++++++++++++ .../src/codegen/container/struct.rs | 12 ++++++ .../src/codegen/module.rs | 6 +++ 4 files changed, 78 insertions(+), 14 deletions(-) diff --git a/crates/stackable-versioned-macros/src/codegen/container/enum.rs b/crates/stackable-versioned-macros/src/codegen/container/enum.rs index bf9c735c5..ed2244e63 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/enum.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/enum.rs @@ -1,6 +1,6 @@ use std::ops::Not; -use darling::{FromAttributes, Result}; +use darling::{util::IdentString, FromAttributes, Result}; use proc_macro2::TokenStream; use quote::quote; use syn::ItemEnum; @@ -88,14 +88,19 @@ impl Container { } } +/// A versioned enum. pub(crate) struct Enum { /// List of variants defined in the original enum. How, and if, an item /// should generate code, is decided by the currently generated version. pub(crate) variants: Vec, + + /// Common container data which is shared between enums and structs. pub(crate) common: CommonContainerData, } +// Common token generation impl Enum { + /// Generates code for the enum definition. pub(crate) fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { let original_attributes = &self.common.original_attributes; let ident = &self.common.idents.original; @@ -115,6 +120,7 @@ impl Enum { } } + /// Generates code for the `From for NextVersion` implementation. pub(crate) fn generate_from_impl( &self, version: &VersionDefinition, @@ -133,14 +139,7 @@ impl Enum { let next_version_ident = &next_version.ident; let version_ident = &version.ident; - let mut variants = TokenStream::new(); - for variant in &self.variants { - variants.extend(variant.generate_for_from_impl( - version, - next_version, - enum_ident, - )); - } + let variants = self.generate_from_variants(version, next_version, enum_ident); // Include allow(deprecated) only when this or the next version is // deprecated. Also include it, when a variant in this or the next @@ -172,13 +171,29 @@ impl Enum { } } + /// Generates code for enum variants used in `From` implementations. + fn generate_from_variants( + &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)); + } + + tokens + } + /// Returns whether any variant is deprecated in the provided `version`. fn is_any_variant_deprecated(&self, version: &VersionDefinition) -> bool { - // First, iterate over all variants. The `any` function will return true if any of the - // function invocations return true. If a field doesn't have a chain, - // we can safely default to false (unversioned fields cannot be - // deprecated). Then we retrieve the status of the field and ensure it - // is deprecated. + // First, iterate over all variants. The `any` function will return true + // if any of the function invocations return true. If a variant doesn't + // have a chain, we can safely default to false (unversioned variants + // cannot be deprecated). Then we retrieve the status of the variant and + // ensure it is deprecated. self.variants.iter().any(|f| { f.changes.as_ref().map_or(false, |c| { c.value_is(&version.inner, |a| { diff --git a/crates/stackable-versioned-macros/src/codegen/container/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/mod.rs index 1b07e696d..cef4eb88d 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/mod.rs @@ -15,6 +15,7 @@ use crate::{ mod r#enum; mod r#struct; +/// Contains common container data shared between structs and enums. pub(crate) struct CommonContainerData { /// Original attributes placed on the container, like `#[derive()]` or `#[cfg()]`. pub(crate) original_attributes: Vec, @@ -26,12 +27,18 @@ pub(crate) struct CommonContainerData { pub(crate) idents: ContainerIdents, } +/// Supported types of containers, structs and enums. +/// +/// Abstracting away with kind of container is generated makes it possible to create a list of +/// containers when the macro is used on modules. This enum provides functions to generate code +/// which then internally call the appropriate function based on the variant. pub(crate) enum Container { Struct(Struct), Enum(Enum), } impl Container { + /// Generates the container definition for the specified `version`. pub(crate) fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { match self { Container::Struct(s) => s.generate_definition(version), @@ -39,6 +46,7 @@ impl Container { } } + /// Generates the container `From for NextVersion` implementation. pub(crate) fn generate_from_impl( &self, version: &VersionDefinition, @@ -51,6 +59,16 @@ impl Container { } } + /// Generates Kubernetes specific code snippets. + /// + /// This function returns three values: + /// + /// - an enum variant ident, + /// - an enum variant display string, + /// - and a `CustomResource::crd()` call + /// + /// This function only returns `Some` if it is a struct. Enums cannot be used to define + /// Kubernetes custom resources. pub(crate) fn generate_kubernetes_item( &self, version: &VersionDefinition, @@ -61,6 +79,10 @@ impl Container { } } + /// Generates Kubernetes specific code to merge two or more CRDs into one. + /// + /// This function only returns `Some` if it is a struct. Enums cannot be used to define + /// Kubernetes custom resources. pub(crate) fn generate_kubernetes_merge_crds( &self, enum_variant_idents: Vec, @@ -80,6 +102,12 @@ impl Container { } } +/// A versioned standalone container. +/// +/// A standalone container is a container defined outside of a versioned module. See [`Module`][1] +/// for more information about versioned modules. +/// +/// [1]: crate::codegen::module::Module pub(crate) struct StandaloneContainer { versions: Vec, container: Container, @@ -87,6 +115,7 @@ pub(crate) struct StandaloneContainer { } impl StandaloneContainer { + /// Creates a new versioned standalone struct. pub(crate) fn new_struct( item_struct: ItemStruct, attributes: StandaloneContainerAttributes, @@ -103,6 +132,7 @@ impl StandaloneContainer { }) } + /// Creates a new versioned standalone enum. pub(crate) fn new_enum( item_enum: ItemEnum, attributes: StandaloneContainerAttributes, @@ -119,6 +149,7 @@ impl StandaloneContainer { }) } + /// Generate tokens containing every piece of code required for a standalone container. pub(crate) fn generate_tokens(&self) -> TokenStream { let vis = &self.vis; diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct.rs b/crates/stackable-versioned-macros/src/codegen/container/struct.rs index e478d3d73..fcf7ee368 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct.rs @@ -119,15 +119,19 @@ impl Container { } } +/// A versioned struct. pub(crate) struct Struct { /// List of fields defined in the original struct. How, and if, an item /// should generate code, is decided by the currently generated version. pub(crate) fields: Vec, + + /// Common container data which is shared between structs and enums. pub(crate) common: CommonContainerData, } // Common token generation impl Struct { + /// Generates code for the struct definition. pub(crate) fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { let original_attributes = &self.common.original_attributes; let ident = &self.common.idents.original; @@ -151,6 +155,7 @@ impl Struct { } } + /// Generates code for the `From for NextVersion` implementation. pub(crate) fn generate_from_impl( &self, version: &VersionDefinition, @@ -202,6 +207,7 @@ impl Struct { } } + /// Generates code for struct fields used in `From` implementations. fn generate_from_fields( &self, version: &VersionDefinition, @@ -217,7 +223,13 @@ impl Struct { tokens } + /// Returns whether any field is deprecated in the provided `version`. fn is_any_field_deprecated(&self, version: &VersionDefinition) -> bool { + // First, iterate over all fields. The `any` function will return true + // if any of the function invocations return true. If a field doesn't + // have a chain, we can safely default to false (unversioned fields + // cannot be deprecated). Then we retrieve the status of the field and + // ensure it is deprecated. self.fields.iter().any(|f| { f.changes.as_ref().map_or(false, |c| { c.value_is(&version.inner, |a| { diff --git a/crates/stackable-versioned-macros/src/codegen/module.rs b/crates/stackable-versioned-macros/src/codegen/module.rs index 358f59cf1..be2bcc1f3 100644 --- a/crates/stackable-versioned-macros/src/codegen/module.rs +++ b/crates/stackable-versioned-macros/src/codegen/module.rs @@ -12,6 +12,10 @@ pub(crate) struct ModuleInput { pub(crate) ident: Ident, } +/// A versioned module. +/// +/// Versioned modules allow versioning multiple containers at once without introducing conflicting +/// version module definitions. pub(crate) struct Module { versions: Vec, containers: Vec, @@ -21,6 +25,7 @@ pub(crate) struct Module { } impl Module { + /// Creates a new versioned module containing versioned containers. pub(crate) fn new( ModuleInput { ident, vis, .. }: ModuleInput, preserve_module: bool, @@ -36,6 +41,7 @@ impl Module { } } + /// Generates tokens for all versioned containers. pub(crate) fn generate_tokens(&self) -> TokenStream { if self.containers.is_empty() { return quote! {};