Skip to content

Commit 207a9f8

Browse files
authored
feat(stackable-versioned): Re-emit and merge modules in versioned modules (#971)
* chore: Add changelog entry * feat: Support to re-emit and merge modules in versioned modules * chore: Update changelog link * test: Add new submodule snapshot test * docs: Add submodule section to doc comments * refactor: Emit errors instead of ignoring invalid code * test: Add UI tests for invalid submodules * docs: Adjust doc comments
1 parent f90d042 commit 207a9f8

File tree

9 files changed

+255
-81
lines changed

9 files changed

+255
-81
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#[versioned(version(name = "v1alpha1"), version(name = "v1"))]
2+
// ---
3+
mod versioned {
4+
mod v1alpha1 {
5+
pub use my::reexport::v1alpha1::*;
6+
}
7+
8+
struct Foo {
9+
bar: usize,
10+
}
11+
}

crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@submodule.rs.snap

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/stackable-versioned-macros/src/codegen/module.rs

Lines changed: 116 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,132 @@
11
use std::{collections::HashMap, ops::Not};
22

3-
use darling::util::IdentString;
3+
use darling::{util::IdentString, Error, Result};
44
use proc_macro2::TokenStream;
55
use quote::quote;
6-
use syn::{token::Pub, Ident, Visibility};
6+
use syn::{token::Pub, Ident, Item, ItemMod, ItemUse, Visibility};
77

8-
use crate::codegen::{container::Container, VersionDefinition};
8+
use crate::{
9+
codegen::{container::Container, VersionDefinition},
10+
ModuleAttributes,
11+
};
912

1013
pub(crate) type KubernetesItems = (Vec<TokenStream>, Vec<IdentString>, Vec<String>);
1114

12-
pub(crate) struct ModuleInput {
13-
pub(crate) vis: Visibility,
14-
pub(crate) ident: Ident,
15-
}
16-
1715
/// A versioned module.
1816
///
1917
/// Versioned modules allow versioning multiple containers at once without introducing conflicting
2018
/// version module definitions.
21-
pub(crate) struct Module {
19+
pub struct Module {
2220
versions: Vec<VersionDefinition>,
21+
22+
// Recognized items of the module
2323
containers: Vec<Container>,
24-
preserve_module: bool,
25-
skip_from: bool,
24+
submodules: HashMap<IdentString, Vec<ItemUse>>,
25+
2626
ident: IdentString,
2727
vis: Visibility,
28+
29+
// Flags which influence generation
30+
preserve_module: bool,
31+
skip_from: bool,
2832
}
2933

3034
impl Module {
3135
/// Creates a new versioned module containing versioned containers.
32-
pub(crate) fn new(
33-
ModuleInput { ident, vis, .. }: ModuleInput,
34-
preserve_module: bool,
35-
skip_from: bool,
36-
versions: Vec<VersionDefinition>,
37-
containers: Vec<Container>,
38-
) -> Self {
39-
Self {
40-
ident: ident.into(),
36+
pub fn new(item_mod: ItemMod, module_attributes: ModuleAttributes) -> Result<Self> {
37+
let Some((_, items)) = item_mod.content else {
38+
return Err(
39+
Error::custom("the macro can only be used on module blocks").with_span(&item_mod)
40+
);
41+
};
42+
43+
let versions: Vec<VersionDefinition> = (&module_attributes).into();
44+
45+
let preserve_module = module_attributes
46+
.common
47+
.options
48+
.preserve_module
49+
.is_present();
50+
51+
let skip_from = module_attributes
52+
.common
53+
.options
54+
.skip
55+
.as_ref()
56+
.map_or(false, |opts| opts.from.is_present());
57+
58+
let mut errors = Error::accumulator();
59+
let mut submodules = HashMap::new();
60+
let mut containers = Vec::new();
61+
62+
for item in items {
63+
match item {
64+
Item::Enum(item_enum) => {
65+
let container = Container::new_enum_nested(item_enum, &versions)?;
66+
containers.push(container);
67+
}
68+
Item::Struct(item_struct) => {
69+
let container = Container::new_struct_nested(item_struct, &versions)?;
70+
containers.push(container);
71+
}
72+
Item::Mod(submodule) => {
73+
if !versions
74+
.iter()
75+
.any(|v| v.ident.as_ident() == &submodule.ident)
76+
{
77+
errors.push(
78+
Error::custom(
79+
"submodules must use names which are defined as `version`s",
80+
)
81+
.with_span(&submodule),
82+
);
83+
continue;
84+
}
85+
86+
match submodule.content {
87+
Some((_, items)) => {
88+
let use_statements: Vec<ItemUse> = items
89+
.into_iter()
90+
// We are only interested in use statements. Everything else is ignored.
91+
.filter_map(|item| match item {
92+
Item::Use(item_use) => Some(item_use),
93+
item => {
94+
errors.push(
95+
Error::custom(
96+
"submodules must only define `use` statements",
97+
)
98+
.with_span(&item),
99+
);
100+
101+
None
102+
}
103+
})
104+
.collect();
105+
106+
submodules.insert(submodule.ident.into(), use_statements);
107+
}
108+
None => errors.push(
109+
Error::custom("submodules must be module blocks").with_span(&submodule),
110+
),
111+
}
112+
}
113+
_ => continue,
114+
};
115+
}
116+
117+
errors.finish_with(Self {
118+
ident: item_mod.ident.into(),
119+
vis: item_mod.vis,
41120
preserve_module,
42121
containers,
122+
submodules,
43123
skip_from,
44124
versions,
45-
vis,
46-
}
125+
})
47126
}
48127

49128
/// Generates tokens for all versioned containers.
50-
pub(crate) fn generate_tokens(&self) -> TokenStream {
129+
pub fn generate_tokens(&self) -> TokenStream {
51130
if self.containers.is_empty() {
52131
return quote! {};
53132
}
@@ -103,6 +182,8 @@ impl Module {
103182
}
104183
}
105184

185+
let submodule_imports = self.generate_submodule_imports(version);
186+
106187
// Only add #[automatically_derived] here if the user doesn't want to preserve the
107188
// module.
108189
let automatically_derived = self
@@ -122,6 +203,8 @@ impl Module {
122203
#version_module_vis mod #version_ident {
123204
use super::*;
124205

206+
#submodule_imports
207+
125208
#container_definitions
126209
}
127210

@@ -163,4 +246,14 @@ impl Module {
163246
}
164247
}
165248
}
249+
250+
/// Optionally generates imports (which can be re-exports) located in submodules for the
251+
/// specified `version`.
252+
fn generate_submodule_imports(&self, version: &VersionDefinition) -> Option<TokenStream> {
253+
self.submodules.get(&version.ident).map(|use_statements| {
254+
quote! {
255+
#(#use_statements)*
256+
}
257+
})
258+
}
166259
}

crates/stackable-versioned-macros/src/lib.rs

Lines changed: 72 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ use syn::{spanned::Spanned, Error, Item};
44

55
use crate::{
66
attrs::{container::StandaloneContainerAttributes, module::ModuleAttributes},
7-
codegen::{
8-
container::{Container, StandaloneContainer},
9-
module::{Module, ModuleInput},
10-
VersionDefinition,
11-
},
7+
codegen::{container::StandaloneContainer, module::Module},
128
};
139

1410
#[cfg(test)]
@@ -265,6 +261,73 @@ mod utils;
265261
/// }
266262
/// ```
267263
///
264+
/// ### Re-emitting and merging Submodules
265+
///
266+
/// Modules defined in the versioned module will be re-emitted. This allows for
267+
/// composition of re-exports to compose easier to use imports for downstream
268+
/// consumers of versioned containers. The following rules apply:
269+
///
270+
/// 1. Only modules named the same like defined versions will be re-emitted.
271+
/// Using modules with invalid names will return an error.
272+
/// 2. Only `use` statements defined in the module will be emitted. Declaring
273+
/// other items will return an error.
274+
///
275+
/// ```
276+
/// # use stackable_versioned_macros::versioned;
277+
/// # mod a {
278+
/// # pub mod v1alpha1 {}
279+
/// # }
280+
/// # mod b {
281+
/// # pub mod v1alpha1 {}
282+
/// # }
283+
/// #[versioned(
284+
/// version(name = "v1alpha1"),
285+
/// version(name = "v1")
286+
/// )]
287+
/// mod versioned {
288+
/// mod v1alpha1 {
289+
/// pub use a::v1alpha1::*;
290+
/// pub use b::v1alpha1::*;
291+
/// }
292+
///
293+
/// struct Foo {
294+
/// bar: usize,
295+
/// }
296+
/// }
297+
/// # fn main() {}
298+
/// ```
299+
///
300+
/// <details>
301+
/// <summary>Expand Generated Code</summary>
302+
///
303+
/// ```ignore
304+
/// mod v1alpha1 {
305+
/// use super::*;
306+
/// pub use a::v1alpha1::*;
307+
/// pub use b::v1alpha1::*;
308+
/// pub struct Foo {
309+
/// pub bar: usize,
310+
/// }
311+
/// }
312+
///
313+
/// impl ::std::convert::From<v1alpha1::Foo> for v1::Foo {
314+
/// fn from(__sv_foo: v1alpha1::Foo) -> Self {
315+
/// Self {
316+
/// bar: __sv_foo.bar.into(),
317+
/// }
318+
/// }
319+
/// }
320+
///
321+
/// mod v1 {
322+
/// use super::*;
323+
/// pub struct Foo {
324+
/// pub bar: usize,
325+
/// }
326+
/// }
327+
/// ```
328+
///
329+
/// </details>
330+
///
268331
/// ## Item Actions
269332
///
270333
/// 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::
802865
Err(err) => return err.write_errors(),
803866
};
804867

805-
let versions: Vec<VersionDefinition> = (&module_attributes).into();
806-
let preserve_modules = module_attributes
807-
.common
808-
.options
809-
.preserve_module
810-
.is_present();
811-
812-
let skip_from = module_attributes
813-
.common
814-
.options
815-
.skip
816-
.as_ref()
817-
.map_or(false, |opts| opts.from.is_present());
818-
819-
let module_span = item_mod.span();
820-
let module_input = ModuleInput {
821-
ident: item_mod.ident,
822-
vis: item_mod.vis,
823-
};
824-
825-
let Some((_, items)) = item_mod.content else {
826-
return Error::new(module_span, "the macro can only be used on module blocks")
827-
.into_compile_error();
868+
let module = match Module::new(item_mod, module_attributes) {
869+
Ok(module) => module,
870+
Err(err) => return err.write_errors(),
828871
};
829872

830-
let mut containers = Vec::new();
831-
832-
for item in items {
833-
let container = match item {
834-
Item::Enum(item_enum) => {
835-
match Container::new_enum_nested(item_enum, &versions) {
836-
Ok(container) => container,
837-
Err(err) => return err.write_errors(),
838-
}
839-
}
840-
Item::Struct(item_struct) => {
841-
match Container::new_struct_nested(item_struct, &versions) {
842-
Ok(container) => container,
843-
Err(err) => return err.write_errors(),
844-
}
845-
}
846-
_ => continue,
847-
};
848-
849-
containers.push(container);
850-
}
851-
852-
Module::new(
853-
module_input,
854-
preserve_modules,
855-
skip_from,
856-
versions,
857-
containers,
858-
)
859-
.generate_tokens()
873+
module.generate_tokens()
860874
}
861875
Item::Enum(item_enum) => {
862876
let container_attributes: StandaloneContainerAttributes =
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
use stackable_versioned_macros::versioned;
2+
3+
fn main() {
4+
#[versioned(version(name = "v1alpha1"))]
5+
mod versioned {
6+
mod v1alpha2 {}
7+
}
8+
}

crates/stackable-versioned-macros/tests/default/fail/submodule_invalid_name.stderr

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)