Skip to content

Commit cca721a

Browse files
feat(stackable-versioned): Add support for versioned enums (#813)
* Update changelog * Update PR link in the changelog * Start to move code, add enum impls * Introduce generalized structs for containers and items * Finish traits and generic types * Use From<&ContainerAttributes> for Vec<ContainerVersion> implementation * Finish basic enum code generation * Use darling(flatten) for field attrs * Replace unwraps with expects * Generate code for all item states * Start adding From ipls for enum conversion * Finish basic From impls for enums * Apply suggestions Co-authored-by: Nick <NickLarsenNZ@users.noreply.github.com> * Apply more suggestions Co-authored-by: Nick <NickLarsenNZ@users.noreply.github.com> * Rename starts_with variable to starts_with_deprecated * Remove old todo comment * Add auto-generated notes for deprecated versions * Move attribute parsing into new() functions --------- Co-authored-by: Nick <NickLarsenNZ@users.noreply.github.com>
1 parent 3c825bf commit cca721a

File tree

22 files changed

+1162
-272
lines changed

22 files changed

+1162
-272
lines changed

Cargo.lock

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ chrono = { version = "0.4.38", default-features = false }
1616
clap = { version = "4.5.4", features = ["derive", "cargo", "env"] }
1717
const_format = "0.2.32"
1818
const-oid = "0.9.6"
19+
convert_case = "0.6.0"
1920
darling = "0.20.9"
2021
delegate = "0.12.0"
2122
derivative = "2.2.0"

crates/stackable-versioned-macros/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ proc-macro = true
1212
[dependencies]
1313
k8s-version = { path = "../k8s-version", features = ["darling"] }
1414

15+
convert_case.workspace = true
1516
darling.workspace = true
1617
itertools.workspace = true
1718
proc-macro2.workspace = true

crates/stackable-versioned-macros/src/attrs/container.rs renamed to crates/stackable-versioned-macros/src/attrs/common/container.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,13 @@ impl ContainerAttributes {
4949
.sort_by(|lhs, rhs| lhs.name.partial_cmp(&rhs.name).unwrap_or(Ordering::Equal));
5050

5151
for (index, version) in original.iter().enumerate() {
52-
if version.name == self.versions.get(index).unwrap().name {
52+
if version.name
53+
== self
54+
.versions
55+
.get(index)
56+
.expect("internal error: version at that index must exist")
57+
.name
58+
{
5359
continue;
5460
}
5561

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use darling::{util::SpannedValue, Error, FromMeta};
2+
use k8s_version::Version;
3+
use proc_macro2::Span;
4+
use syn::Path;
5+
6+
/// These attributes are meant to be used in super structs, which add
7+
/// [`Field`](syn::Field) or [`Variant`](syn::Variant) specific attributes via
8+
/// darling's flatten feature. This struct only provides shared attributes.
9+
#[derive(Debug, FromMeta)]
10+
#[darling(and_then = ItemAttributes::validate)]
11+
pub(crate) struct ItemAttributes {
12+
/// This parses the `added` attribute on items (fields or variants). It can
13+
/// only be present at most once.
14+
pub(crate) added: Option<AddedAttributes>,
15+
16+
/// This parses the `renamed` attribute on items (fields or variants). It
17+
/// can be present 0..n times.
18+
#[darling(multiple, rename = "renamed")]
19+
pub(crate) renames: Vec<RenamedAttributes>,
20+
21+
/// This parses the `deprecated` attribute on items (fields or variants). It
22+
/// can only be present at most once.
23+
pub(crate) deprecated: Option<DeprecatedAttributes>,
24+
}
25+
26+
impl ItemAttributes {
27+
fn validate(self) -> Result<Self, Error> {
28+
// Validate deprecated options
29+
30+
// TODO (@Techassi): Make the field 'note' optional, because in the
31+
// future, the macro will generate parts of the deprecation note
32+
// automatically. The user-provided note will then be appended to the
33+
// auto-generated one.
34+
35+
if let Some(deprecated) = &self.deprecated {
36+
if deprecated.note.is_empty() {
37+
return Err(Error::custom("deprecation note must not be empty")
38+
.with_span(&deprecated.note.span()));
39+
}
40+
}
41+
42+
Ok(self)
43+
}
44+
}
45+
46+
/// For the added() action
47+
///
48+
/// Example usage:
49+
/// - `added(since = "...")`
50+
/// - `added(since = "...", default_fn = "custom_fn")`
51+
#[derive(Clone, Debug, FromMeta)]
52+
pub(crate) struct AddedAttributes {
53+
pub(crate) since: SpannedValue<Version>,
54+
55+
#[darling(rename = "default", default = "default_default_fn")]
56+
pub(crate) default_fn: SpannedValue<Path>,
57+
}
58+
59+
fn default_default_fn() -> SpannedValue<Path> {
60+
SpannedValue::new(
61+
syn::parse_str("std::default::Default::default").expect("internal error: path must parse"),
62+
Span::call_site(),
63+
)
64+
}
65+
66+
/// For the renamed() action
67+
///
68+
/// Example usage:
69+
/// - `renamed(since = "...", from = "...")`
70+
#[derive(Clone, Debug, FromMeta)]
71+
pub(crate) struct RenamedAttributes {
72+
pub(crate) since: SpannedValue<Version>,
73+
pub(crate) from: SpannedValue<String>,
74+
}
75+
76+
/// For the deprecated() action
77+
///
78+
/// Example usage:
79+
/// - `deprecated(since = "...", note = "...")`
80+
#[derive(Clone, Debug, FromMeta)]
81+
pub(crate) struct DeprecatedAttributes {
82+
pub(crate) since: SpannedValue<Version>,
83+
pub(crate) note: SpannedValue<String>,
84+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod container;
2+
mod item;
3+
4+
pub(crate) use container::*;
5+
pub(crate) use item::*;

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

+37-73
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
use darling::{util::SpannedValue, Error, FromField, FromMeta};
2-
use k8s_version::Version;
3-
use proc_macro2::Span;
4-
use syn::{Field, Ident, Path};
1+
use darling::{Error, FromField};
2+
use syn::{Field, Ident};
53

6-
use crate::{attrs::container::ContainerAttributes, consts::DEPRECATED_PREFIX};
4+
use crate::{
5+
attrs::common::{ContainerAttributes, ItemAttributes},
6+
consts::DEPRECATED_FIELD_PREFIX,
7+
};
78

89
/// This struct describes all available field attributes, as well as the field
910
/// name to display better diagnostics.
@@ -29,43 +30,24 @@ use crate::{attrs::container::ContainerAttributes, consts::DEPRECATED_PREFIX};
2930
and_then = FieldAttributes::validate
3031
)]
3132
pub(crate) struct FieldAttributes {
32-
pub(crate) ident: Option<Ident>,
33-
pub(crate) added: Option<AddedAttributes>,
34-
35-
#[darling(multiple, rename = "renamed")]
36-
pub(crate) renames: Vec<RenamedAttributes>,
37-
38-
pub(crate) deprecated: Option<DeprecatedAttributes>,
39-
}
40-
41-
#[derive(Clone, Debug, FromMeta)]
42-
pub(crate) struct AddedAttributes {
43-
pub(crate) since: SpannedValue<Version>,
44-
45-
#[darling(rename = "default", default = "default_default_fn")]
46-
pub(crate) default_fn: SpannedValue<Path>,
47-
}
48-
49-
fn default_default_fn() -> SpannedValue<Path> {
50-
SpannedValue::new(
51-
syn::parse_str("std::default::Default::default").expect("internal error: path must parse"),
52-
Span::call_site(),
53-
)
54-
}
55-
56-
#[derive(Clone, Debug, FromMeta)]
57-
pub(crate) struct RenamedAttributes {
58-
pub(crate) since: SpannedValue<Version>,
59-
pub(crate) from: SpannedValue<String>,
60-
}
33+
#[darling(flatten)]
34+
pub(crate) common: ItemAttributes,
6135

62-
#[derive(Clone, Debug, FromMeta)]
63-
pub(crate) struct DeprecatedAttributes {
64-
pub(crate) since: SpannedValue<Version>,
65-
pub(crate) note: SpannedValue<String>,
36+
// The ident (automatically extracted by darling) cannot be moved into the
37+
// shared item attributes because for struct fields, the type is
38+
// `Option<Ident>`, while for enum variants, the type is `Ident`.
39+
pub(crate) ident: Option<Ident>,
6640
}
6741

6842
impl FieldAttributes {
43+
// NOTE (@Techassi): Ideally, these validations should be moved to the
44+
// ItemAttributes impl, because common validation like action combinations
45+
// and action order can be validated without taking the type of attribute
46+
// into account (field vs variant). However, we would loose access to the
47+
// field / variant ident and as such, cannot display the error directly on
48+
// the affected field / variant. This is a significant decrease in DX.
49+
// See https://github.com/TedDriggs/darling/discussions/294
50+
6951
/// This associated function is called by darling (see and_then attribute)
7052
/// after it successfully parsed the attribute. This allows custom
7153
/// validation of the attribute which extends the validation already in
@@ -80,12 +62,6 @@ impl FieldAttributes {
8062
errors.handle(self.validate_action_order());
8163
errors.handle(self.validate_field_name());
8264

83-
// Code quality validation
84-
errors.handle(self.validate_deprecated_options());
85-
86-
// TODO (@Techassi): Add validation for renames so that renamed fields
87-
// match up and form a continous chain (eg. foo -> bar -> baz).
88-
8965
// TODO (@Techassi): Add hint if a field is added in the first version
9066
// that it might be clever to remove the 'added' attribute.
9167

@@ -107,7 +83,11 @@ impl FieldAttributes {
10783
/// - `renamed` and `deprecated` using the same version: Again, the same
10884
/// rules from above apply here as well.
10985
fn validate_action_combinations(&self) -> Result<(), Error> {
110-
match (&self.added, &self.renames, &self.deprecated) {
86+
match (
87+
&self.common.added,
88+
&self.common.renames,
89+
&self.common.deprecated,
90+
) {
11191
(Some(added), _, Some(deprecated)) if *added.since == *deprecated.since => {
11292
Err(Error::custom(
11393
"field cannot be marked as `added` and `deprecated` in the same version",
@@ -145,15 +125,15 @@ impl FieldAttributes {
145125
/// - All `renamed` actions must use a greater version than `added` but a
146126
/// lesser version than `deprecated`.
147127
fn validate_action_order(&self) -> Result<(), Error> {
148-
let added_version = self.added.as_ref().map(|a| *a.since);
149-
let deprecated_version = self.deprecated.as_ref().map(|d| *d.since);
128+
let added_version = self.common.added.as_ref().map(|a| *a.since);
129+
let deprecated_version = self.common.deprecated.as_ref().map(|d| *d.since);
150130

151131
// First, validate that the added version is less than the deprecated
152132
// version.
153133
// NOTE (@Techassi): Is this already covered by the code below?
154134
if let (Some(added_version), Some(deprecated_version)) = (added_version, deprecated_version)
155135
{
156-
if added_version >= deprecated_version {
136+
if added_version > deprecated_version {
157137
return Err(Error::custom(format!(
158138
"field was marked as `added` in version `{added_version}` while being marked as `deprecated` in an earlier version `{deprecated_version}`"
159139
)).with_span(&self.ident));
@@ -162,7 +142,7 @@ impl FieldAttributes {
162142

163143
// Now, iterate over all renames and ensure that their versions are
164144
// between the added and deprecated version.
165-
if !self.renames.iter().all(|r| {
145+
if !self.common.renames.iter().all(|r| {
166146
added_version.map_or(true, |a| a < *r.since)
167147
&& deprecated_version.map_or(true, |d| d > *r.since)
168148
}) {
@@ -185,20 +165,20 @@ impl FieldAttributes {
185165
/// in their name. The prefix must not be included for fields which are
186166
/// not deprecated.
187167
fn validate_field_name(&self) -> Result<(), Error> {
188-
let starts_with = self
168+
let starts_with_deprecated = self
189169
.ident
190170
.as_ref()
191-
.unwrap()
171+
.expect("internal error: to be validated fields must have a name")
192172
.to_string()
193-
.starts_with(DEPRECATED_PREFIX);
173+
.starts_with(DEPRECATED_FIELD_PREFIX);
194174

195-
if self.deprecated.is_some() && !starts_with {
175+
if self.common.deprecated.is_some() && !starts_with_deprecated {
196176
return Err(Error::custom(
197177
"field was marked as `deprecated` and thus must include the `deprecated_` prefix in its name"
198178
).with_span(&self.ident));
199179
}
200180

201-
if self.deprecated.is_none() && starts_with {
181+
if self.common.deprecated.is_none() && starts_with_deprecated {
202182
return Err(Error::custom(
203183
"field includes the `deprecated_` prefix in its name but is not marked as `deprecated`"
204184
).with_span(&self.ident));
@@ -207,22 +187,6 @@ impl FieldAttributes {
207187
Ok(())
208188
}
209189

210-
fn validate_deprecated_options(&self) -> Result<(), Error> {
211-
// TODO (@Techassi): Make the field 'note' optional, because in the
212-
// future, the macro will generate parts of the deprecation note
213-
// automatically. The user-provided note will then be appended to the
214-
// auto-generated one.
215-
216-
if let Some(deprecated) = &self.deprecated {
217-
if deprecated.note.is_empty() {
218-
return Err(Error::custom("deprecation note must not be empty")
219-
.with_span(&deprecated.note.span()));
220-
}
221-
}
222-
223-
Ok(())
224-
}
225-
226190
/// Validates that each field action version is present in the declared
227191
/// container versions.
228192
pub(crate) fn validate_versions(
@@ -233,7 +197,7 @@ impl FieldAttributes {
233197
// NOTE (@Techassi): Can we maybe optimize this a little?
234198
let mut errors = Error::accumulator();
235199

236-
if let Some(added) = &self.added {
200+
if let Some(added) = &self.common.added {
237201
if !container_attrs
238202
.versions
239203
.iter()
@@ -246,7 +210,7 @@ impl FieldAttributes {
246210
}
247211
}
248212

249-
for rename in &self.renames {
213+
for rename in &self.common.renames {
250214
if !container_attrs
251215
.versions
252216
.iter()
@@ -259,7 +223,7 @@ impl FieldAttributes {
259223
}
260224
}
261225

262-
if let Some(deprecated) = &self.deprecated {
226+
if let Some(deprecated) = &self.common.deprecated {
263227
if !container_attrs
264228
.versions
265229
.iter()
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
pub(crate) mod container;
1+
pub(crate) mod common;
22
pub(crate) mod field;
3+
pub(crate) mod variant;

0 commit comments

Comments
 (0)