Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#[versioned(version(name = "v1alpha1"), version(name = "v1"))]
// ---
mod versioned {
mod v1alpha1 {
pub use my::reexport::v1alpha1::*;
}

struct Foo {
bar: usize,
}
}

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

132 changes: 109 additions & 23 deletions crates/stackable-versioned-macros/src/codegen/module.rs
Original file line number Diff line number Diff line change
@@ -1,53 +1,114 @@
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<TokenStream>, Vec<IdentString>, Vec<String>);

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<VersionDefinition>,

// Recognized items of the module
containers: Vec<Container>,
preserve_module: bool,
skip_from: bool,
submodules: Vec<Submodule>,

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<VersionDefinition>,
containers: Vec<Container>,
) -> Self {
Self {
ident: ident.into(),
pub fn new(item_mod: ItemMod, module_attributes: ModuleAttributes) -> Result<Self> {
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<VersionDefinition> = (&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 containers = Vec::new();
let mut submodules = 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(item_module) => match item_module.content {
Some((_, items)) => {
let imports: Vec<ItemUse> = items
.into_iter()
// We are only interested in use statements. Everything else is ignored.
// NOTE (@Techassi): We might want to error instead.
.filter_map(|item| match item {
Item::Use(item_use) => Some(item_use),
_ => None,
})
.collect();

// NOTE (@Techassi): Do we wanna enforce that modules must use version names?

submodules.push(Submodule {
ident: item_module.ident.into(),
imports,
});
}
None => errors.push(
Error::custom("submodules must be module blocks").with_span(&item_module),
),
},
_ => 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! {};
}
Expand Down Expand Up @@ -103,6 +164,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
Expand All @@ -122,6 +185,8 @@ impl Module {
#version_module_vis mod #version_ident {
use super::*;

#submodule_imports

#container_definitions
}

Expand Down Expand Up @@ -163,4 +228,25 @@ 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<TokenStream> {
for submodule in &self.submodules {
if submodule.ident == version.ident {
let imports = &submodule.imports;

return Some(quote! {
#(#imports)*
});
}
}

None
}
}

pub struct Submodule {
ident: IdentString,
imports: Vec<ItemUse>,
}
132 changes: 74 additions & 58 deletions crates/stackable-versioned-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -265,6 +261,75 @@ 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.
/// Modules named differently will be ignored and won't produce any code. In
/// the future, this might return an error instead.
/// 2. Only `use` statements defined in the module will be emitted. Other items
/// will be ignored and won't produce any code. In the future, this might
/// return an error instead.
///
/// ```
/// # 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() {}
/// ```
///
/// <details>
/// <summary>Expand Generated Code</summary>
///
/// ```ignore
/// mod v1alpha1 {
/// use super::*;
/// pub use a::v1alpha1::*;
/// pub use b::v1alpha1::*;
/// pub struct Foo {
/// pub bar: usize,
/// }
/// }
///
/// impl ::std::convert::From<v1alpha1::Foo> 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,
/// }
/// }
/// ```
///
/// </details>
///
/// ## Item Actions
///
/// This crate currently supports three different item actions. Items can
Expand Down Expand Up @@ -802,61 +867,12 @@ fn versioned_impl(attrs: proc_macro2::TokenStream, input: Item) -> proc_macro2::
Err(err) => return err.write_errors(),
};

let versions: Vec<VersionDefinition> = (&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 =
Expand Down
2 changes: 2 additions & 0 deletions crates/stackable-versioned/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down