Skip to content

Commit 494ed37

Browse files
feat(stackable-versioned): Add support for type changes of fields (#844)
* chore: Update changelog * chore: Update PR link in changelog * refactor: Rename renamed action to changed * fix: Update doc test * feat: Basic support for type changes The current implementation is very brittle and less than ideal. It currently only produces correct code in the struct definitions, but not in the From impls. For that reason, test files currently skip the From impl generation. I have a bunch of thoughts on how to improve the situation, but I would like to tackle these changes in follow-up commits. * feat: Make note of deprecated action optional * feat: Generate correct From impls on type changes * chore: Update changelog * chore: Apply suggestions Co-authored-by: Nick <[email protected]> --------- Co-authored-by: Nick <[email protected]>
1 parent 96217cf commit 494ed37

File tree

14 files changed

+269
-137
lines changed

14 files changed

+269
-137
lines changed

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

+38-42
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use darling::{util::SpannedValue, Error, FromMeta};
22
use k8s_version::Version;
33
use proc_macro2::Span;
4-
use syn::{spanned::Spanned, Attribute, Ident, Path};
4+
use syn::{spanned::Spanned, Attribute, Ident, Path, Type};
55

66
use crate::{
77
attrs::common::ContainerAttributes,
@@ -55,14 +55,14 @@ where
5555
}
5656
}
5757

58-
for rename in &*self.common_attributes().renames {
58+
for change in &*self.common_attributes().changes {
5959
if !container_attrs
6060
.versions
6161
.iter()
62-
.any(|v| v.name == *rename.since)
62+
.any(|v| v.name == *change.since)
6363
{
6464
errors.push(
65-
Error::custom("variant action `renamed` uses version which was not declared via #[versioned(version)]")
65+
Error::custom("variant action `changed` uses version which was not declared via #[versioned(version)]")
6666
.with_span(item)
6767
);
6868
}
@@ -97,15 +97,20 @@ pub(crate) enum ItemType {
9797
Variant,
9898
}
9999

100+
// TODO (@Techassi): Shower thought: Track actions as a Vec of an ActionAttribute
101+
// enum and implement Ord on it. This would allow us to order the items by type
102+
// of action (added < changed < deprecated) as well as sort changed action to
103+
// each other by specified version (which already implements Ord)
104+
100105
/// These attributes are meant to be used in super structs, which add
101106
/// [`Field`](syn::Field) or [`Variant`](syn::Variant) specific attributes via
102107
/// darling's flatten feature. This struct only provides shared attributes.
103108
///
104109
/// ### Shared Item Rules
105110
///
106111
/// - An item can only ever be added once at most. An item not marked as 'added'
107-
/// is part of the container in every version until renamed or deprecated.
108-
/// - An item can be renamed many times. That's why renames are stored in a
112+
/// is part of the container in every version until changed or deprecated.
113+
/// - An item can be changed many times. That's why changes are stored in a
109114
/// [`Vec`].
110115
/// - An item can only be deprecated once. A field or variant not marked as
111116
/// 'deprecated' will be included up until the latest version.
@@ -115,10 +120,10 @@ pub(crate) struct ItemAttributes {
115120
/// only be present at most once.
116121
pub(crate) added: Option<AddedAttributes>,
117122

118-
/// This parses the `renamed` attribute on items (fields or variants). It
123+
/// This parses the `changed` attribute on items (fields or variants). It
119124
/// can be present 0..n times.
120-
#[darling(multiple, rename = "renamed")]
121-
pub(crate) renames: Vec<RenamedAttributes>,
125+
#[darling(multiple, rename = "changed")]
126+
pub(crate) changes: Vec<ChangedAttributes>,
122127

123128
/// This parses the `deprecated` attribute on items (fields or variants). It
124129
/// can only be present at most once.
@@ -138,20 +143,6 @@ impl ItemAttributes {
138143

139144
let mut errors = Error::accumulator();
140145

141-
// TODO (@Techassi): Make the field or variant 'note' optional, because
142-
// in the future, the macro will generate parts of the deprecation note
143-
// automatically. The user-provided note will then be appended to the
144-
// auto-generated one.
145-
146-
if let Some(deprecated) = &self.deprecated {
147-
if deprecated.note.is_empty() {
148-
errors.push(
149-
Error::custom("deprecation note must not be empty")
150-
.with_span(&deprecated.note.span()),
151-
);
152-
}
153-
}
154-
155146
// Semantic validation
156147
errors.handle(self.validate_action_combinations(item_ident, item_type));
157148
errors.handle(self.validate_action_order(item_ident, item_type));
@@ -175,34 +166,34 @@ impl ItemAttributes {
175166
/// cannot be marked as added in a particular version and then marked as
176167
/// deprecated immediately after. Fields and variants must be included for
177168
/// at least one version before being marked deprecated.
178-
/// - `added` and `renamed` using the same version: The same reasoning from
169+
/// - `added` and `changed` using the same version: The same reasoning from
179170
/// above applies here as well. Fields and variants must be included for
180-
/// at least one version before being renamed.
181-
/// - `renamed` and `deprecated` using the same version: Again, the same
171+
/// at least one version before being changed.
172+
/// - `changed` and `deprecated` using the same version: Again, the same
182173
/// rules from above apply here as well.
183174
fn validate_action_combinations(
184175
&self,
185176
item_ident: &Ident,
186177
item_type: &ItemType,
187178
) -> Result<(), Error> {
188-
match (&self.added, &self.renames, &self.deprecated) {
179+
match (&self.added, &self.changes, &self.deprecated) {
189180
(Some(added), _, Some(deprecated)) if *added.since == *deprecated.since => {
190181
Err(Error::custom(format!(
191182
"{item_type} cannot be marked as `added` and `deprecated` in the same version"
192183
))
193184
.with_span(item_ident))
194185
}
195-
(Some(added), renamed, _) if renamed.iter().any(|r| *r.since == *added.since) => {
186+
(Some(added), changed, _) if changed.iter().any(|r| *r.since == *added.since) => {
196187
Err(Error::custom(format!(
197-
"{item_type} cannot be marked as `added` and `renamed` in the same version"
188+
"{item_type} cannot be marked as `added` and `changed` in the same version"
198189
))
199190
.with_span(item_ident))
200191
}
201-
(_, renamed, Some(deprecated))
202-
if renamed.iter().any(|r| *r.since == *deprecated.since) =>
192+
(_, changed, Some(deprecated))
193+
if changed.iter().any(|r| *r.since == *deprecated.since) =>
203194
{
204195
Err(Error::custom(
205-
format!("{item_type} cannot be marked as `deprecated` and `renamed` in the same version"),
196+
format!("{item_type} cannot be marked as `deprecated` and `changed` in the same version"),
206197
)
207198
.with_span(item_ident))
208199
}
@@ -220,7 +211,7 @@ impl ItemAttributes {
220211
/// ensures that these versions are chronologically sound, that means,
221212
/// that the version of the deprecated action must be greater than the
222213
/// version of the added action.
223-
/// - All `renamed` actions must use a greater version than `added` but a
214+
/// - All `changed` actions must use a greater version than `added` but a
224215
/// lesser version than `deprecated`.
225216
fn validate_action_order(&self, item_ident: &Ident, item_type: &ItemType) -> Result<(), Error> {
226217
let added_version = self.added.as_ref().map(|a| *a.since);
@@ -238,14 +229,14 @@ impl ItemAttributes {
238229
}
239230
}
240231

241-
// Now, iterate over all renames and ensure that their versions are
232+
// Now, iterate over all changes and ensure that their versions are
242233
// between the added and deprecated version.
243-
if !self.renames.iter().all(|r| {
234+
if !self.changes.iter().all(|r| {
244235
added_version.map_or(true, |a| a < *r.since)
245236
&& deprecated_version.map_or(true, |d| d > *r.since)
246237
}) {
247238
return Err(Error::custom(
248-
"all renames must use versions higher than `added` and lower than `deprecated`",
239+
"all changes must use versions higher than `added` and lower than `deprecated`",
249240
)
250241
.with_span(item_ident));
251242
}
@@ -320,27 +311,32 @@ pub(crate) struct AddedAttributes {
320311

321312
fn default_default_fn() -> SpannedValue<Path> {
322313
SpannedValue::new(
323-
syn::parse_str("std::default::Default::default").expect("internal error: path must parse"),
314+
syn::parse_str("::std::default::Default::default")
315+
.expect("internal error: path must parse"),
324316
Span::call_site(),
325317
)
326318
}
327319

328-
/// For the renamed() action
320+
/// For the changed() action
329321
///
330322
/// Example usage:
331-
/// - `renamed(since = "...", from = "...")`
323+
/// - `changed(since = "...", from_name = "...")`
324+
/// - `changed(since = "...", from_name = "..." from_type="...")`
332325
#[derive(Clone, Debug, FromMeta)]
333-
pub(crate) struct RenamedAttributes {
326+
pub(crate) struct ChangedAttributes {
334327
pub(crate) since: SpannedValue<Version>,
335-
pub(crate) from: SpannedValue<String>,
328+
pub(crate) from_name: Option<SpannedValue<String>>,
329+
330+
pub(crate) from_type: Option<SpannedValue<Type>>,
336331
}
337332

338333
/// For the deprecated() action
339334
///
340335
/// Example usage:
336+
/// - `deprecated(since = "...")`
341337
/// - `deprecated(since = "...", note = "...")`
342338
#[derive(Clone, Debug, FromMeta)]
343339
pub(crate) struct DeprecatedAttributes {
344340
pub(crate) since: SpannedValue<Version>,
345-
pub(crate) note: SpannedValue<String>,
341+
pub(crate) note: Option<SpannedValue<String>>,
346342
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ impl FieldAttributes {
5050
.ident
5151
.as_ref()
5252
.expect("internal error: field must have an ident");
53-
self.common.validate(ident, &ItemType::Field, &self.attrs)?;
5453

54+
self.common.validate(ident, &ItemType::Field, &self.attrs)?;
5555
Ok(self)
5656
}
5757
}

crates/stackable-versioned-macros/src/attrs/variant.rs

+8-6
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,14 @@ impl VariantAttributes {
5555
);
5656

5757
// Validate names of renames
58-
for rename in &self.common.renames {
59-
if !rename.from.is_case(Case::Pascal) {
60-
errors.push(
61-
Error::custom("renamed variant must use PascalCase")
62-
.with_span(&rename.from.span()),
63-
)
58+
for change in &self.common.changes {
59+
if let Some(from_name) = &change.from_name {
60+
if !from_name.is_case(Case::Pascal) {
61+
errors.push(
62+
Error::custom("renamed variant must use PascalCase")
63+
.with_span(&from_name.span()),
64+
)
65+
}
6466
}
6567
}
6668

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ mod test {
9191
#[case(2, (Some(&"test1"), Some(&"test3")))]
9292
#[case(3, (Some(&"test1"), None))]
9393
#[case(4, (Some(&"test3"), None))]
94-
fn test(#[case] key: i32, #[case] expected: (Option<&&str>, Option<&&str>)) {
94+
fn neighbors(#[case] key: i32, #[case] expected: (Option<&&str>, Option<&&str>)) {
9595
let map = BTreeMap::from([(1, "test1"), (3, "test3")]);
9696
let neigbors = map.get_neighbors(&key);
9797

0 commit comments

Comments
 (0)