Skip to content
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

feat(stackable-versioned): Generate From implementations for automatic versions upgrades #790

Merged
merged 19 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 4 additions & 0 deletions crates/stackable-versioned-macros/src/attrs/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ impl ContainerAttributes {
}
}

// TODO (@Techassi): Add validation for skip(from) for last version,
// which will skip nothing, because nothing is generated in the first
// place.

// Ensure every version is unique and isn't declared multiple times. This
// is inspired by the itertools all_unique function.
let mut unique = HashSet::new();
Expand Down
1 change: 1 addition & 0 deletions crates/stackable-versioned-macros/src/attrs/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ impl FieldAttributes {

// First, validate that the added version is less than the deprecated
// version.
// NOTE (@Techassi): Is this already covered by the code below?
if let (Some(added_version), Some(deprecated_version)) = (added_version, deprecated_version)
{
if added_version >= deprecated_version {
Expand Down
2 changes: 2 additions & 0 deletions crates/stackable-versioned-macros/src/gen/vstruct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ impl VersionedStruct {

let fields = self.generate_from_fields(version, next_version, from_ident);

// TODO (@Techassi): Be a little bit more clever about when to include
// the #[allow(deprecated)] attribute.
return quote! {
#[automatically_derived]
#[allow(deprecated)]
Expand Down
216 changes: 216 additions & 0 deletions crates/stackable-versioned-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,222 @@ mod attrs;
mod consts;
mod gen;

/// This macro enables generating versioned structs.
///
/// ## Usage Guide
///
/// ### Quickstart
///
/// ```
/// # use stackable_versioned_macros::versioned;
/// #[versioned(
/// version(name = "v1alpha1"),
/// version(name = "v1beta1"),
/// version(name = "v1"),
/// version(name = "v2"),
/// version(name = "v3")
/// )]
/// struct Foo {
/// /// My docs
/// #[versioned(
/// added(since = "v1beta1"),
/// renamed(since = "v1", from = "gau"),
/// deprecated(since = "v2", note = "not empty")
/// )]
/// deprecated_bar: usize,
/// baz: bool,
/// }
/// ```
///
/// ### Declaring Versions
///
/// Before any of the fields can be versioned, versions need to be declared at
/// the container level. Each version currently supports two parameters: `name`
/// and the `deprecated` flag. The `name` must be a valid (and supported)
/// format. The macro checks each declared version and reports any error
/// encountered during parsing.
/// The `deprecated` flag marks the version as deprecated. This currently adds
/// the `#[deprecated]` attribute to the appropriate piece of code.
///
/// ```
/// # use stackable_versioned_macros::versioned;
/// #[versioned(
/// version(name = "v1alpha1", deprecated)
/// )]
/// struct Foo {}
/// ```
///
/// Additionally, it is ensured that each version is unique. Declaring the same
/// version multiple times will result in an error. Furthermore, declaring the
/// versions out-of-order ist prohibited by default. It is possible to opt-out
Techassi marked this conversation as resolved.
Show resolved Hide resolved
/// of this check by setting `options(allow_unsorted)`:
///
/// ```
/// # use stackable_versioned_macros::versioned;
/// #[versioned(
/// version(name = "v1beta1"),
/// version(name = "v1alpha1"),
/// options(allow_unsorted)
/// )]
/// struct Foo {}
/// ```
///
/// ### Field Actions
///
/// This library currently supports three different field actions. Fields can
/// be added, renamed and deprecated. The macro ensures that these actions
/// adhere to the following set of rules:
///
/// - Fields cannot be added and deprecated in the same version.
/// - Fields cannot be added and renamed in the same version.
/// - Fields cannot be renamed and deprecated in the same version.
/// - Fields added in version _a_, renamed _0...n_ times in versions
/// b<sub>1</sub>, b<sub>2</sub>, ..., b<sub>n</sub> and deprecated in
/// version _c_ must ensure _a < b<sub>1</sub>, b<sub>2</sub>, ...,
/// b<sub>n</sub> < c_.
/// - All field actions must use previously declared versions. Using versions
/// not present at the container level will result in an error.
///
/// ```
/// # use stackable_versioned_macros::versioned;
/// #[versioned(
/// version(name = "v1alpha1"),
/// version(name = "v1beta1"),
/// version(name = "v1"),
/// version(name = "v2"),
/// )]
/// struct Foo {
/// #[versioned(
/// added(since = "v1beta1"),
/// renamed(since = "v1", from = "gau"),
/// deprecated(since = "v2", note = "not empty")
/// )]
/// deprecated_bar: usize,
/// baz: bool,
/// }
Techassi marked this conversation as resolved.
Show resolved Hide resolved
/// ```
///
/// For fields marked as deprecated, two additional rules apply:
///
/// - Fields must start with the `deprecated_` prefix.
/// - The deprecation note cannot be empty.
///
/// ### Auto-generated [`From`] Implementations
///
/// To enable smooth version upgrades of the same struct, the macro automatically
/// generates [`From`] implementations. On a high level, code generated for two
/// versions _a_ and _b_, with _a < b_ looks like this: `impl From<a> for b`.
///
/// ```ignore
/// #[versioned(
/// version(name = "v1alpha1"),
/// version(name = "v1beta1"),
/// version(name = "v1")
/// )]
/// pub struct Foo {
/// #[versioned(
/// added(since = "v1beta1"),
/// deprecated(since = "v1", note = "not needed")
/// )]
/// deprecated_bar: usize,
/// baz: bool,
/// }
///
/// // Produces ...
///
/// #[automatically_derived]
/// pub mod v1alpha1 {
/// pub struct Foo {
/// pub baz: bool,
/// }
/// }
/// #[automatically_derived]
/// #[allow(deprecated)]
/// impl From<v1alpha1::Foo> for v1beta1::Foo {
/// fn from(__sv_foo: v1alpha1::Foo) -> Self {
/// Self {
/// bar: std::default::Default::default(),
/// baz: __sv_foo.baz,
/// }
/// }
/// }
/// #[automatically_derived]
/// pub mod v1beta1 {
/// pub struct Foo {
/// pub bar: usize,
/// pub baz: bool,
/// }
/// }
/// #[automatically_derived]
/// #[allow(deprecated)]
/// impl From<v1beta1::Foo> for v1::Foo {
/// fn from(__sv_foo: v1beta1::Foo) -> Self {
/// Self {
/// deprecated_bar: __sv_foo.bar,
/// baz: __sv_foo.baz,
/// }
/// }
/// }
/// #[automatically_derived]
/// pub mod v1 {
/// pub struct Foo {
/// #[deprecated = "not needed"]
/// pub deprecated_bar: usize,
/// pub baz: bool,
/// }
/// }
/// ```
///
/// #### Skip [`From`] generation
///
/// Generation of these [`From`] implementations can be skipped at the container
/// and version level. This enables customization of the implementations if the
/// default implementation is not sufficient.
///
/// ```
/// # use stackable_versioned_macros::versioned;
/// #[versioned(
/// version(name = "v1alpha1"),
/// version(name = "v1beta1"),
/// version(name = "v1"),
/// options(skip(from))
/// )]
/// pub struct Foo {
/// #[versioned(
/// added(since = "v1beta1"),
/// deprecated(since = "v1", note = "not needed")
/// )]
/// deprecated_bar: usize,
/// baz: bool,
/// }
/// ```
///
/// #### Customize Default Function for Added Fields
///
/// It is possible to customize the default function used in the generated
/// [`From`] implementation for populating added fields. By default,
/// [`Default::default()`] is used.
///
/// ```
/// # use stackable_versioned_macros::versioned;
/// #[versioned(
/// version(name = "v1alpha1"),
/// version(name = "v1beta1"),
/// version(name = "v1")
/// )]
/// pub struct Foo {
/// #[versioned(
/// added(since = "v1beta1", default = "default_bar"),
/// deprecated(since = "v1", note = "not needed")
/// )]
/// deprecated_bar: usize,
/// baz: bool,
/// }
///
/// fn default_bar() -> usize {
/// 42
/// }
/// ```
#[proc_macro_attribute]
pub fn versioned(attrs: TokenStream, input: TokenStream) -> TokenStream {
let attrs = match NestedMeta::parse_meta_list(attrs.into()) {
Expand Down
4 changes: 0 additions & 4 deletions crates/stackable-versioned/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ All notable changes to this project will be documented in this file.
[#790]: https://github.com/stackabletech/operator-rs/pull/790
[#793]: https://github.com/stackabletech/operator-rs/pull/793

- Improve action chain generation ([#784]).

[#784](ttps://github.com/stackabletech/operator-rs/pull/784)

## [0.1.0] - 2024-05-08

### Changed
Expand Down
9 changes: 6 additions & 3 deletions crates/stackable-versioned/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! data type. This will be extended to support SemVer versions, as well as
//! custom version formats in the future.
//!
//! ## Basic Usage
//! ## Usage Guide
//!
//! ```
//! use stackable_versioned::versioned;
Expand All @@ -18,14 +18,17 @@
//! struct Foo {
//! /// My docs
//! #[versioned(
//! added(since = "v1alpha1"),
//! renamed(since = "v1beta1", from = "gau"),
//! added(since = "v1beta1"),
//! renamed(since = "v1", from = "gau"),
//! deprecated(since = "v2", note = "not empty")
//! )]
//! deprecated_bar: usize,
//! baz: bool,
//! }
//! ```
//!
//! See [`versioned`] for an in-depth usage guide and a list of supported
//! parameters.

pub use stackable_versioned_macros::*;

Expand Down
Loading