diff --git a/crates/stackable-versioned-macros/fixtures/inputs/default/submodule.rs b/crates/stackable-versioned-macros/fixtures/inputs/default/submodule.rs new file mode 100644 index 000000000..b1665c729 --- /dev/null +++ b/crates/stackable-versioned-macros/fixtures/inputs/default/submodule.rs @@ -0,0 +1,11 @@ +#[versioned(version(name = "v1alpha1"), version(name = "v1"))] +// --- +mod versioned { + mod v1alpha1 { + pub use my::reexport::v1alpha1::*; + } + + struct Foo { + bar: usize, + } +} diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@submodule.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@submodule.rs.snap new file mode 100644 index 000000000..7b6b9bc3d --- /dev/null +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@submodule.rs.snap @@ -0,0 +1,26 @@ +--- +source: crates/stackable-versioned-macros/src/lib.rs +expression: formatted +input_file: crates/stackable-versioned-macros/fixtures/inputs/default/submodule.rs +--- +#[automatically_derived] +mod v1alpha1 { + use super::*; + pub use my::reexport::v1alpha1::*; + pub struct Foo { + pub bar: usize, + } +} +#[automatically_derived] +impl ::std::convert::From for v1::Foo { + fn from(__sv_foo: v1alpha1::Foo) -> Self { + Self { bar: __sv_foo.bar.into() } + } +} +#[automatically_derived] +mod v1 { + use super::*; + pub struct Foo { + pub bar: usize, + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/module.rs b/crates/stackable-versioned-macros/src/codegen/module.rs index 4ae950c1b..0de782f57 100644 --- a/crates/stackable-versioned-macros/src/codegen/module.rs +++ b/crates/stackable-versioned-macros/src/codegen/module.rs @@ -1,53 +1,132 @@ use std::{collections::HashMap, ops::Not}; -use darling::util::IdentString; +use darling::{util::IdentString, Error, Result}; use proc_macro2::TokenStream; use quote::quote; -use syn::{token::Pub, Ident, Visibility}; +use syn::{token::Pub, Ident, Item, ItemMod, ItemUse, Visibility}; -use crate::codegen::{container::Container, VersionDefinition}; +use crate::{ + codegen::{container::Container, VersionDefinition}, + ModuleAttributes, +}; pub(crate) type KubernetesItems = (Vec, Vec, Vec); -pub(crate) struct ModuleInput { - pub(crate) vis: Visibility, - 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 { +pub struct Module { versions: Vec, + + // Recognized items of the module containers: Vec, - preserve_module: bool, - skip_from: bool, + submodules: HashMap>, + ident: IdentString, vis: Visibility, + + // Flags which influence generation + preserve_module: bool, + skip_from: bool, } impl Module { /// Creates a new versioned module containing versioned containers. - pub(crate) fn new( - ModuleInput { ident, vis, .. }: ModuleInput, - preserve_module: bool, - skip_from: bool, - versions: Vec, - containers: Vec, - ) -> Self { - Self { - ident: ident.into(), + pub fn new(item_mod: ItemMod, module_attributes: ModuleAttributes) -> Result { + let Some((_, items)) = item_mod.content else { + return Err( + Error::custom("the macro can only be used on module blocks").with_span(&item_mod) + ); + }; + + let versions: Vec = (&module_attributes).into(); + + let preserve_module = module_attributes + .common + .options + .preserve_module + .is_present(); + + let skip_from = module_attributes + .common + .options + .skip + .as_ref() + .map_or(false, |opts| opts.from.is_present()); + + let mut errors = Error::accumulator(); + let mut submodules = HashMap::new(); + let mut containers = Vec::new(); + + for item in items { + match item { + Item::Enum(item_enum) => { + let container = Container::new_enum_nested(item_enum, &versions)?; + containers.push(container); + } + Item::Struct(item_struct) => { + let container = Container::new_struct_nested(item_struct, &versions)?; + containers.push(container); + } + Item::Mod(submodule) => { + if !versions + .iter() + .any(|v| v.ident.as_ident() == &submodule.ident) + { + errors.push( + Error::custom( + "submodules must use names which are defined as `version`s", + ) + .with_span(&submodule), + ); + continue; + } + + match submodule.content { + Some((_, items)) => { + let use_statements: Vec = items + .into_iter() + // We are only interested in use statements. Everything else is ignored. + .filter_map(|item| match item { + Item::Use(item_use) => Some(item_use), + item => { + errors.push( + Error::custom( + "submodules must only define `use` statements", + ) + .with_span(&item), + ); + + None + } + }) + .collect(); + + submodules.insert(submodule.ident.into(), use_statements); + } + None => errors.push( + Error::custom("submodules must be module blocks").with_span(&submodule), + ), + } + } + _ => continue, + }; + } + + errors.finish_with(Self { + ident: item_mod.ident.into(), + vis: item_mod.vis, preserve_module, containers, + submodules, skip_from, versions, - vis, - } + }) } /// Generates tokens for all versioned containers. - pub(crate) fn generate_tokens(&self) -> TokenStream { + pub fn generate_tokens(&self) -> TokenStream { if self.containers.is_empty() { return quote! {}; } @@ -103,6 +182,8 @@ impl Module { } } + let submodule_imports = self.generate_submodule_imports(version); + // Only add #[automatically_derived] here if the user doesn't want to preserve the // module. let automatically_derived = self @@ -122,6 +203,8 @@ impl Module { #version_module_vis mod #version_ident { use super::*; + #submodule_imports + #container_definitions } @@ -163,4 +246,14 @@ impl Module { } } } + + /// Optionally generates imports (which can be re-exports) located in submodules for the + /// specified `version`. + fn generate_submodule_imports(&self, version: &VersionDefinition) -> Option { + self.submodules.get(&version.ident).map(|use_statements| { + quote! { + #(#use_statements)* + } + }) + } } diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs index b27f12ce8..0d67b8ef7 100644 --- a/crates/stackable-versioned-macros/src/lib.rs +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -4,11 +4,7 @@ use syn::{spanned::Spanned, Error, Item}; use crate::{ attrs::{container::StandaloneContainerAttributes, module::ModuleAttributes}, - codegen::{ - container::{Container, StandaloneContainer}, - module::{Module, ModuleInput}, - VersionDefinition, - }, + codegen::{container::StandaloneContainer, module::Module}, }; #[cfg(test)] @@ -265,6 +261,73 @@ mod utils; /// } /// ``` /// +/// ### Re-emitting and merging Submodules +/// +/// Modules defined in the versioned module will be re-emitted. This allows for +/// composition of re-exports to compose easier to use imports for downstream +/// consumers of versioned containers. The following rules apply: +/// +/// 1. Only modules named the same like defined versions will be re-emitted. +/// Using modules with invalid names will return an error. +/// 2. Only `use` statements defined in the module will be emitted. Declaring +/// other items will return an error. +/// +/// ``` +/// # use stackable_versioned_macros::versioned; +/// # mod a { +/// # pub mod v1alpha1 {} +/// # } +/// # mod b { +/// # pub mod v1alpha1 {} +/// # } +/// #[versioned( +/// version(name = "v1alpha1"), +/// version(name = "v1") +/// )] +/// mod versioned { +/// mod v1alpha1 { +/// pub use a::v1alpha1::*; +/// pub use b::v1alpha1::*; +/// } +/// +/// struct Foo { +/// bar: usize, +/// } +/// } +/// # fn main() {} +/// ``` +/// +///
+/// Expand Generated Code +/// +/// ```ignore +/// mod v1alpha1 { +/// use super::*; +/// pub use a::v1alpha1::*; +/// pub use b::v1alpha1::*; +/// pub struct Foo { +/// pub bar: usize, +/// } +/// } +/// +/// impl ::std::convert::From for v1::Foo { +/// fn from(__sv_foo: v1alpha1::Foo) -> Self { +/// Self { +/// bar: __sv_foo.bar.into(), +/// } +/// } +/// } +/// +/// mod v1 { +/// use super::*; +/// pub struct Foo { +/// pub bar: usize, +/// } +/// } +/// ``` +/// +///
+/// /// ## Item Actions /// /// This crate currently supports three different item actions. Items can @@ -802,61 +865,12 @@ fn versioned_impl(attrs: proc_macro2::TokenStream, input: Item) -> proc_macro2:: Err(err) => return err.write_errors(), }; - let versions: Vec = (&module_attributes).into(); - let preserve_modules = module_attributes - .common - .options - .preserve_module - .is_present(); - - let skip_from = module_attributes - .common - .options - .skip - .as_ref() - .map_or(false, |opts| opts.from.is_present()); - - let module_span = item_mod.span(); - let module_input = ModuleInput { - ident: item_mod.ident, - vis: item_mod.vis, - }; - - let Some((_, items)) = item_mod.content else { - return Error::new(module_span, "the macro can only be used on module blocks") - .into_compile_error(); + let module = match Module::new(item_mod, module_attributes) { + Ok(module) => module, + Err(err) => return err.write_errors(), }; - 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, - skip_from, - versions, - containers, - ) - .generate_tokens() + module.generate_tokens() } Item::Enum(item_enum) => { let container_attributes: StandaloneContainerAttributes = diff --git a/crates/stackable-versioned-macros/tests/default/fail/submodule_invalid_name.rs b/crates/stackable-versioned-macros/tests/default/fail/submodule_invalid_name.rs new file mode 100644 index 000000000..2ce7a5997 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/default/fail/submodule_invalid_name.rs @@ -0,0 +1,8 @@ +use stackable_versioned_macros::versioned; + +fn main() { + #[versioned(version(name = "v1alpha1"))] + mod versioned { + mod v1alpha2 {} + } +} diff --git a/crates/stackable-versioned-macros/tests/default/fail/submodule_invalid_name.stderr b/crates/stackable-versioned-macros/tests/default/fail/submodule_invalid_name.stderr new file mode 100644 index 000000000..b213f2ebd --- /dev/null +++ b/crates/stackable-versioned-macros/tests/default/fail/submodule_invalid_name.stderr @@ -0,0 +1,5 @@ +error: submodules must use names which are defined as `version`s + --> tests/default/fail/submodule_invalid_name.rs:6:9 + | +6 | mod v1alpha2 {} + | ^^^ diff --git a/crates/stackable-versioned-macros/tests/default/fail/submodule_use_statement.rs b/crates/stackable-versioned-macros/tests/default/fail/submodule_use_statement.rs new file mode 100644 index 000000000..041451ab0 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/default/fail/submodule_use_statement.rs @@ -0,0 +1,10 @@ +use stackable_versioned::versioned; + +fn main() { + #[versioned(version(name = "v1alpha1"))] + mod versioned { + mod v1alpha1 { + struct Foo; + } + } +} diff --git a/crates/stackable-versioned-macros/tests/default/fail/submodule_use_statement.stderr b/crates/stackable-versioned-macros/tests/default/fail/submodule_use_statement.stderr new file mode 100644 index 000000000..966b1061b --- /dev/null +++ b/crates/stackable-versioned-macros/tests/default/fail/submodule_use_statement.stderr @@ -0,0 +1,5 @@ +error: submodules must only define `use` statements + --> tests/default/fail/submodule_use_statement.rs:7:13 + | +7 | struct Foo; + | ^^^^^^ diff --git a/crates/stackable-versioned/CHANGELOG.md b/crates/stackable-versioned/CHANGELOG.md index 0060a3a6b..72be15bb7 100644 --- a/crates/stackable-versioned/CHANGELOG.md +++ b/crates/stackable-versioned/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added +- Add support for re-emitting and merging modules defined in versioned modules ([#971]). - Add basic support for generic types in struct and enum definitions ([#969]). ### Changed @@ -14,6 +15,7 @@ All notable changes to this project will be documented in this file. [#961]: https://github.com/stackabletech/operator-rs/pull/961 [#969]: https://github.com/stackabletech/operator-rs/pull/969 +[#971]: https://github.com/stackabletech/operator-rs/pull/971 ## [0.5.1] - 2025-02-14