Skip to content

Commit 9538541

Browse files
committed
feat(stackable-versioned): Add argument to hint for wrapped types
1 parent f5d0ce9 commit 9538541

File tree

7 files changed

+367
-96
lines changed

7 files changed

+367
-96
lines changed

crates/stackable-versioned-macros/src/attrs/item/field.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use darling::{Error, FromField, Result, util::Flag};
1+
use darling::{Error, FromField, FromMeta, Result, util::Flag};
22
use syn::{Attribute, Ident};
33

44
use crate::{
@@ -44,6 +44,10 @@ pub struct FieldAttributes {
4444
/// is needed to let the macro know to generate conversion code with support
4545
/// for tracking across struct boundaries.
4646
pub nested: Flag,
47+
48+
/// Provide a hint if a field is wrapped in either `Option` or `Vec` to
49+
/// generate correct code in the `From` impl blocks.
50+
pub hint: Option<Hint>,
4751
}
4852

4953
impl FieldAttributes {
@@ -81,3 +85,10 @@ impl FieldAttributes {
8185
Ok(())
8286
}
8387
}
88+
89+
/// Supported field hints.
90+
#[derive(Debug, FromMeta)]
91+
pub enum Hint {
92+
Option,
93+
Vec,
94+
}

crates/stackable-versioned-macros/src/codegen/item/field.rs

Lines changed: 153 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ use darling::{FromField, Result, util::IdentString};
44
use k8s_version::Version;
55
use proc_macro2::TokenStream;
66
use quote::quote;
7-
use syn::{Attribute, Field, Ident, Type};
7+
use syn::{Attribute, Field, Ident, Path, Type};
88

99
use crate::{
10-
attrs::item::FieldAttributes,
10+
attrs::item::{FieldAttributes, Hint},
1111
codegen::{
1212
Direction, VersionDefinition,
1313
changes::{BTreeMapExt, ChangesetExt},
@@ -21,6 +21,7 @@ pub struct VersionedField {
2121
pub original_attributes: Vec<Attribute>,
2222
pub changes: Option<BTreeMap<Version, ItemStatus>>,
2323
pub idents: FieldIdents,
24+
pub hint: Option<Hint>,
2425
pub nested: bool,
2526
pub ty: Type,
2627
}
@@ -47,6 +48,7 @@ impl VersionedField {
4748

4849
Ok(Self {
4950
original_attributes: field_attributes.attrs,
51+
hint: field_attributes.hint,
5052
ty: field.ty,
5153
changes,
5254
idents,
@@ -60,19 +62,31 @@ impl VersionedField {
6062
}
6163
}
6264

65+
/// Generates field definitions for the use inside container (struct) definitions.
66+
///
67+
/// This function needs to take into account multiple conditions:
68+
///
69+
/// - Only emit the field if it exists for the currently generated version.
70+
/// - Emit field with new name and type if there was a name and/or type change.
71+
/// - Handle deprecated fields accordingly.
72+
///
73+
/// ### Example
74+
///
75+
/// ```ignore
76+
/// struct Foo {
77+
/// bar: usize, // < This functions generates one or more of these definitions
78+
/// }
79+
/// ```
6380
pub fn generate_for_container(&self, version: &VersionDefinition) -> Option<TokenStream> {
6481
let original_attributes = &self.original_attributes;
6582

6683
match &self.changes {
6784
Some(changes) => {
68-
// Check if the provided container version is present in the map
69-
// of actions. If it is, some action occurred in exactly that
70-
// version and thus code is generated for that field based on
71-
// the type of action.
72-
// If not, the provided version has no action attached to it.
73-
// The code generation then depends on the relation to other
74-
// versions (with actions).
75-
85+
// Check if the provided container version is present in the map of actions. If it
86+
// is, some action occurred in exactly that version and thus code is generated for
87+
// that field based on the type of action.
88+
// If not, the provided version has no action attached to it. The code generation
89+
// then depends on the relation to other versions (with actions).
7690
let field_type = &self.ty;
7791

7892
// NOTE (@Techassi): https://rust-lang.github.io/rust-clippy/master/index.html#/expect_fun_call
@@ -97,14 +111,12 @@ impl VersionedField {
97111
note,
98112
..
99113
} => {
100-
// FIXME (@Techassi): Emitting the deprecated attribute
101-
// should cary over even when the item status is
102-
// 'NoChange'.
103-
// TODO (@Techassi): Make the generation of deprecated
104-
// items customizable. When a container is used as a K8s
105-
// CRD, the item must continue to exist, even when
106-
// deprecated. For other versioning use-cases, that
107-
// might not be the case.
114+
// FIXME (@Techassi): Emitting the deprecated attribute should cary over even
115+
// when the item status is 'NoChange'.
116+
// TODO (@Techassi): Make the generation of deprecated items customizable.
117+
// When a container is used as a K8s CRD, the item must continue to exist,
118+
// even when deprecated. For other versioning use-cases, that might not be
119+
// the case.
108120
let deprecated_attr = if let Some(note) = note {
109121
quote! {#[deprecated = #note]}
110122
} else {
@@ -124,8 +136,7 @@ impl VersionedField {
124136
ty,
125137
..
126138
} => {
127-
// TODO (@Techassi): Also carry along the deprecation
128-
// note.
139+
// TODO (@Techassi): Also carry along the deprecation note.
129140
let deprecated_attr = previously_deprecated.then(|| quote! {#[deprecated]});
130141

131142
Some(quote! {
@@ -137,8 +148,8 @@ impl VersionedField {
137148
}
138149
}
139150
None => {
140-
// If there is no chain of field actions, the field is not
141-
// versioned and therefore included in all versions.
151+
// If there is no chain of field actions, the field is not versioned and therefore
152+
// included in all versions.
142153
let field_ident = &self.idents.original;
143154
let field_type = &self.ty;
144155

@@ -150,6 +161,27 @@ impl VersionedField {
150161
}
151162
}
152163

164+
/// Generates field definitions for the use inside `From` impl blocks.
165+
///
166+
/// This function needs to take into account multiple conditions:
167+
///
168+
/// - Only emit the field if it exists for the currently generated version.
169+
/// - Emit fields which previously didn't exist with the correct initialization function.
170+
/// - Emit field with new name and type if there was a name and/or type change.
171+
/// - Handle tracking conversions without data-loss.
172+
/// - Handle deprecated fields accordingly.
173+
///
174+
/// ### Example
175+
///
176+
/// ```ignore
177+
/// impl From<v1alpha1::Foo> for v1alpha2::Foo {
178+
/// fn from(value: v1alpha1::Foo) -> Self {
179+
/// Self {
180+
/// bar: value.bar, // < This functions generates one or more of these definitions
181+
/// }
182+
/// }
183+
/// }
184+
/// ```
153185
pub fn generate_for_from_impl(
154186
&self,
155187
direction: Direction,
@@ -163,9 +195,9 @@ impl VersionedField {
163195
let change = changes.get_expect(&version.inner);
164196

165197
match (change, next_change) {
166-
// If both this status and the next one is NotPresent, which means
167-
// a field was introduced after a bunch of versions, we don't
168-
// need to generate any code for the From impl.
198+
// If both this status and the next one is NotPresent, which means a field was
199+
// introduced after a bunch of versions, we don't need to generate any code for
200+
// the From impl.
169201
(ItemStatus::NotPresent, ItemStatus::NotPresent) => None,
170202
(
171203
_,
@@ -186,93 +218,57 @@ impl VersionedField {
186218
..
187219
},
188220
) => match direction {
189-
Direction::Upgrade => match upgrade_with {
190-
// The user specified a custom conversion function which
191-
// will be used here instead of the default .into() call
192-
// which utilizes From impls.
193-
Some(upgrade_fn) => Some(quote! {
194-
#to_ident: #upgrade_fn(#from_struct_ident.#from_ident),
195-
}),
196-
// Default .into() call using From impls.
197-
None => {
198-
if self.nested {
199-
let json_path_ident = to_ident.json_path_ident();
200-
201-
Some(quote! {
202-
#to_ident: #from_struct_ident.#from_ident.tracking_into(status, &#json_path_ident),
203-
})
204-
} else {
205-
Some(quote! {
206-
#to_ident: #from_struct_ident.#from_ident.into(),
207-
})
208-
}
209-
}
210-
},
211-
Direction::Downgrade => match downgrade_with {
212-
Some(downgrade_fn) => Some(quote! {
213-
#from_ident: #downgrade_fn(#from_struct_ident.#to_ident),
214-
}),
215-
None => {
216-
if self.nested {
217-
let json_path_ident = from_ident.json_path_ident();
218-
219-
Some(quote! {
220-
#from_ident: #from_struct_ident.#to_ident.tracking_into(status, &#json_path_ident),
221-
})
222-
} else {
223-
Some(quote! {
224-
#from_ident: #from_struct_ident.#to_ident.into(),
225-
})
226-
}
227-
}
228-
},
221+
Direction::Upgrade => Some(self.generate_from_impl_field(
222+
to_ident,
223+
from_struct_ident,
224+
from_ident,
225+
upgrade_with.as_ref(),
226+
)),
227+
Direction::Downgrade => Some(self.generate_from_impl_field(
228+
from_ident,
229+
from_struct_ident,
230+
to_ident,
231+
downgrade_with.as_ref(),
232+
)),
229233
},
230234
(old, next) => {
231235
let next_field_ident = next.get_ident();
232236
let old_field_ident = old.get_ident();
233237

234-
// NOTE (@Techassi): Do we really need .into() here. I'm
235-
// currently not sure why it is there and if it is needed
236-
// in some edge cases.
238+
// NOTE (@Techassi): Do we really need .into() here. I'm currently not sure
239+
// why it is there and if it is needed in some edge cases.
237240
match direction {
238-
Direction::Upgrade => {
239-
if self.nested {
240-
let json_path_ident = next_field_ident.json_path_ident();
241-
242-
Some(quote! {
243-
#next_field_ident: #from_struct_ident.#old_field_ident.tracking_into(status, &#json_path_ident),
244-
})
245-
} else {
246-
Some(quote! {
247-
#next_field_ident: #from_struct_ident.#old_field_ident.into(),
248-
})
249-
}
250-
}
251-
Direction::Downgrade => Some(quote! {
252-
#old_field_ident: #from_struct_ident.#next_field_ident.into(),
253-
}),
241+
Direction::Upgrade => Some(self.generate_from_impl_field(
242+
next_field_ident,
243+
from_struct_ident,
244+
old_field_ident,
245+
None,
246+
)),
247+
Direction::Downgrade => Some(self.generate_from_impl_field(
248+
old_field_ident,
249+
from_struct_ident,
250+
next_field_ident,
251+
None,
252+
)),
254253
}
255254
}
256255
}
257256
}
258257
None => {
259258
let field_ident = &self.idents.original;
260259

261-
if self.nested {
262-
let json_path_ident = field_ident.json_path_ident();
263-
264-
Some(quote! {
265-
#field_ident: #from_struct_ident.#field_ident.tracking_into(status, &#json_path_ident),
266-
})
267-
} else {
268-
Some(quote! {
269-
#field_ident: #from_struct_ident.#field_ident.into(),
270-
})
271-
}
260+
Some(self.generate_from_impl_field(
261+
field_ident,
262+
from_struct_ident,
263+
field_ident,
264+
None,
265+
))
272266
}
273267
}
274268
}
275269

270+
/// Generates code needed when a tracked conversion for this field needs to be inserted into the
271+
/// status.
276272
pub fn generate_for_status_insertion(
277273
&self,
278274
direction: Direction,
@@ -315,6 +311,8 @@ impl VersionedField {
315311
}
316312
}
317313

314+
/// Generates code needed when a tracked conversion for this field needs to be removed from the
315+
/// status.
318316
pub fn generate_for_status_removal(
319317
&self,
320318
direction: Direction,
@@ -389,6 +387,66 @@ impl VersionedField {
389387
}
390388
}
391389
}
390+
391+
/// Generates field definitions to be used inside `From` impl blocks.
392+
fn generate_from_impl_field(
393+
&self,
394+
lhs_field_ident: &IdentString,
395+
rhs_struct_ident: &IdentString,
396+
rhs_field_ident: &IdentString,
397+
custom_conversion_function: Option<&Path>,
398+
) -> TokenStream {
399+
match custom_conversion_function {
400+
// The user specified a custom conversion function which will be used here instead of the
401+
// default conversion call which utilizes From impls.
402+
Some(convert_fn) => quote! {
403+
#lhs_field_ident: #convert_fn(#rhs_struct_ident.#rhs_field_ident),
404+
},
405+
// Default conversion call using From impls.
406+
None => {
407+
if self.nested {
408+
let json_path_ident = lhs_field_ident.json_path_ident();
409+
let func = self.generate_tracking_conversion_function(json_path_ident);
410+
411+
quote! {
412+
#lhs_field_ident: #rhs_struct_ident.#rhs_field_ident.#func,
413+
}
414+
} else {
415+
let func = self.generate_conversion_function();
416+
417+
quote! {
418+
#lhs_field_ident: #rhs_struct_ident.#rhs_field_ident.#func,
419+
}
420+
}
421+
}
422+
}
423+
}
424+
425+
/// Generates tracking conversion functions used by field definitions in `From` impl blocks.
426+
fn generate_tracking_conversion_function(&self, json_path_ident: IdentString) -> TokenStream {
427+
match &self.hint {
428+
Some(hint) => match hint {
429+
Hint::Option => {
430+
quote! { map(|v| v.tracking_into(status, &#json_path_ident)) }
431+
}
432+
Hint::Vec => {
433+
quote! { into_iter().map(|v| v.tracking_into(status, &#json_path_ident)).collect() }
434+
}
435+
},
436+
None => quote! { tracking_into(status, &#json_path_ident) },
437+
}
438+
}
439+
440+
/// Generates conversion functions used by field definitions in `From` impl blocks.
441+
fn generate_conversion_function(&self) -> TokenStream {
442+
match &self.hint {
443+
Some(hint) => match hint {
444+
Hint::Option => quote! { map(Into::into) },
445+
Hint::Vec => quote! { into_iter().map(Into::into).collect() },
446+
},
447+
None => quote! { into() },
448+
}
449+
}
392450
}
393451

394452
#[derive(Debug)]

0 commit comments

Comments
 (0)