Skip to content

feat(stackable-versioned): Re-emit and merge modules in versioned modules #971

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

139 changes: 116 additions & 23 deletions crates/stackable-versioned-macros/src/codegen/module.rs
Original file line number Diff line number Diff line change
@@ -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<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: HashMap<IdentString, Vec<ItemUse>>,

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 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<ItemUse> = 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! {};
}
Expand Down Expand Up @@ -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
Expand All @@ -122,6 +203,8 @@ impl Module {
#version_module_vis mod #version_ident {
use super::*;

#submodule_imports

#container_definitions
}

Expand Down Expand Up @@ -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<TokenStream> {
self.submodules.get(&version.ident).map(|use_statements| {
quote! {
#(#use_statements)*
}
})
}
}
130 changes: 72 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,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() {}
/// ```
///
/// <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 +865,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use stackable_versioned_macros::versioned;

fn main() {
#[versioned(version(name = "v1alpha1"))]
mod versioned {
mod v1alpha2 {}
}
}

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

Loading