diff --git a/Cargo.lock b/Cargo.lock index 9df29c16b..f3b529338 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2403,9 +2403,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.7" +version = "0.23.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebbbdb961df0ad3f2652da8f3fdc4b36122f568f968f45ad3316f26c025c677b" +checksum = "79adb16721f56eb2d843e67676896a61ce7a0fa622dc18d3e372477a029d2740" dependencies = [ "aws-lc-rs", "log", @@ -2913,6 +2913,13 @@ dependencies = [ [[package]] name = "stackable-versioned" version = "0.1.0" +dependencies = [ + "stackable-versioned-macros", +] + +[[package]] +name = "stackable-versioned-macros" +version = "0.1.0" dependencies = [ "darling", "k8s-version", diff --git a/crates/stackable-versioned-macros/Cargo.toml b/crates/stackable-versioned-macros/Cargo.toml new file mode 100644 index 000000000..2c8f98291 --- /dev/null +++ b/crates/stackable-versioned-macros/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "stackable-versioned-macros" +version = "0.1.0" +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true + +[lib] +proc-macro = true + +[dependencies] +k8s-version = { path = "../k8s-version", features = ["darling"] } + +darling.workspace = true +proc-macro2.workspace = true +syn.workspace = true +quote.workspace = true + +[dev-dependencies] +rstest.workspace = true diff --git a/crates/stackable-versioned/src/attrs/container.rs b/crates/stackable-versioned-macros/src/attrs/container.rs similarity index 84% rename from crates/stackable-versioned/src/attrs/container.rs rename to crates/stackable-versioned-macros/src/attrs/container.rs index b0314bd26..fe7191e7c 100644 --- a/crates/stackable-versioned/src/attrs/container.rs +++ b/crates/stackable-versioned-macros/src/attrs/container.rs @@ -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(); @@ -83,10 +87,12 @@ impl ContainerAttributes { /// /// - `name` of the version, like `v1alpha1`. /// - `deprecated` flag to mark that version as deprecated. +/// - `skip` option to skip generating various pieces of code. #[derive(Clone, Debug, FromMeta)] pub(crate) struct VersionAttributes { pub(crate) deprecated: Flag, pub(crate) name: Version, + pub(crate) skip: Option, } /// This struct contains supported container options. @@ -95,7 +101,19 @@ pub(crate) struct VersionAttributes { /// /// - `allow_unsorted`, which allows declaring versions in unsorted order, /// instead of enforcing ascending order. +/// - `skip` option to skip generating various pieces of code. #[derive(Clone, Debug, Default, FromMeta)] pub(crate) struct ContainerOptions { pub(crate) allow_unsorted: Flag, + pub(crate) skip: Option, +} + +/// This struct contains supported skip options. +/// +/// Supported options are: +/// +/// - `from` flag, which skips generating [`From`] implementations when provided. +#[derive(Clone, Debug, Default, FromMeta)] +pub(crate) struct SkipOptions { + pub(crate) from: Flag, } diff --git a/crates/stackable-versioned/src/attrs/field.rs b/crates/stackable-versioned-macros/src/attrs/field.rs similarity index 96% rename from crates/stackable-versioned/src/attrs/field.rs rename to crates/stackable-versioned-macros/src/attrs/field.rs index 24f65a11e..03d0c6045 100644 --- a/crates/stackable-versioned/src/attrs/field.rs +++ b/crates/stackable-versioned-macros/src/attrs/field.rs @@ -1,6 +1,7 @@ use darling::{util::SpannedValue, Error, FromField, FromMeta}; use k8s_version::Version; -use syn::{Field, Ident}; +use proc_macro2::Span; +use syn::{Field, Ident, Path}; use crate::{attrs::container::ContainerAttributes, consts::DEPRECATED_PREFIX}; @@ -40,6 +41,16 @@ pub(crate) struct FieldAttributes { #[derive(Clone, Debug, FromMeta)] pub(crate) struct AddedAttributes { pub(crate) since: SpannedValue, + + #[darling(rename = "default", default = "default_default_fn")] + pub(crate) default_fn: SpannedValue, +} + +fn default_default_fn() -> SpannedValue { + SpannedValue::new( + syn::parse_str("std::default::Default::default").unwrap(), + Span::call_site(), + ) } #[derive(Clone, Debug, FromMeta)] @@ -139,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 { diff --git a/crates/stackable-versioned/src/attrs/mod.rs b/crates/stackable-versioned-macros/src/attrs/mod.rs similarity index 100% rename from crates/stackable-versioned/src/attrs/mod.rs rename to crates/stackable-versioned-macros/src/attrs/mod.rs diff --git a/crates/stackable-versioned/src/consts.rs b/crates/stackable-versioned-macros/src/consts.rs similarity index 100% rename from crates/stackable-versioned/src/consts.rs rename to crates/stackable-versioned-macros/src/consts.rs diff --git a/crates/stackable-versioned/src/gen/field.rs b/crates/stackable-versioned-macros/src/gen/field.rs similarity index 71% rename from crates/stackable-versioned/src/gen/field.rs rename to crates/stackable-versioned-macros/src/gen/field.rs index e6948310b..d5748c8dc 100644 --- a/crates/stackable-versioned/src/gen/field.rs +++ b/crates/stackable-versioned-macros/src/gen/field.rs @@ -1,15 +1,15 @@ -use std::collections::BTreeMap; +use std::{collections::BTreeMap, ops::Deref}; use darling::Error; use k8s_version::Version; use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Field, Ident}; +use syn::{Field, Ident, Path}; use crate::{ attrs::field::FieldAttributes, consts::DEPRECATED_PREFIX, - gen::{neighbors::Neighbors, version::ContainerVersion, ToTokensExt}, + gen::{neighbors::Neighbors, version::ContainerVersion}, }; /// A versioned field, which contains contains common [`Field`] data and a chain @@ -17,65 +17,13 @@ use crate::{ /// /// The chain of action maps versions to an action and the appropriate field /// name. Additionally, the [`Field`] data can be used to forward attributes, -/// generate documention, etc. +/// generate documentation, etc. #[derive(Debug)] pub(crate) struct VersionedField { chain: Option>, inner: Field, } -impl ToTokensExt for VersionedField { - fn to_tokens_for_version(&self, container_version: &ContainerVersion) -> Option { - match &self.chain { - Some(chain) => { - // Check if the provided container version is present in the map - // of actions. If it is, some action occured in exactly that - // version and thus code is generated for that field based on - // the type of action. - // If not, the provided version has no action attached to it. - // The code generation then depends on the relation to other - // versions (with actions). - - let field_type = &self.inner.ty; - - match chain - .get(&container_version.inner) - .expect("internal error: chain must contain container version") - { - FieldStatus::Added(field_ident) => Some(quote! { - pub #field_ident: #field_type, - }), - FieldStatus::Renamed { _from: _, to } => Some(quote! { - pub #to: #field_type, - }), - FieldStatus::Deprecated { - ident: field_ident, - note, - } => Some(quote! { - #[deprecated = #note] - pub #field_ident: #field_type, - }), - FieldStatus::NotPresent => None, - FieldStatus::NoChange(field_ident) => Some(quote! { - pub #field_ident: #field_type, - }), - } - } - None => { - // If there is no chain of field actions, the field is not - // versioned and code generation is straight forward. - // Unversioned fields are always included in versioned structs. - let field_ident = &self.inner.ident; - let field_type = &self.inner.ty; - - Some(quote! { - pub #field_ident: #field_type, - }) - } - } - } -} - impl VersionedField { /// Create a new versioned field by creating a status chain for each version /// defined in an action in the field attribute. @@ -95,9 +43,9 @@ impl VersionedField { // The ident of the deprecated field is guaranteed to include the // 'deprecated_' prefix. The ident can thus be used as is. if let Some(deprecated) = attrs.deprecated { + let ident = field.ident.as_ref().unwrap(); let mut actions = BTreeMap::new(); - let ident = field.ident.as_ref().unwrap(); actions.insert( *deprecated.since, FieldStatus::Deprecated { @@ -106,7 +54,7 @@ impl VersionedField { }, ); - // When the field is deprecated, any rename which occured beforehand + // When the field is deprecated, any rename which occurred beforehand // requires access to the field ident to infer the field ident for // the latest rename. let mut ident = format_ident!( @@ -129,7 +77,13 @@ impl VersionedField { // After the last iteration above (if any) we use the ident for the // added action if there is any. if let Some(added) = attrs.added { - actions.insert(*added.since, FieldStatus::Added(ident)); + actions.insert( + *added.since, + FieldStatus::Added { + default_fn: added.default_fn.deref().clone(), + ident, + }, + ); } Ok(Self { @@ -155,11 +109,15 @@ impl VersionedField { // After the last iteration above (if any) we use the ident for the // added action if there is any. if let Some(added) = attrs.added { - actions.insert(*added.since, FieldStatus::Added(ident)); + actions.insert( + *added.since, + FieldStatus::Added { + default_fn: added.default_fn.deref().clone(), + ident, + }, + ); } - dbg!(&actions); - Ok(Self { chain: Some(actions), inner: field, @@ -170,7 +128,10 @@ impl VersionedField { actions.insert( *added.since, - FieldStatus::Added(field.ident.clone().unwrap()), + FieldStatus::Added { + default_fn: added.default_fn.deref().clone(), + ident: field.ident.clone().unwrap(), + }, ); return Ok(Self { @@ -188,12 +149,12 @@ impl VersionedField { /// Inserts container versions not yet present in the status chain. /// - /// When intially creating a new [`VersionedField`], the code doesn't have + /// When initially creating a new [`VersionedField`], the code doesn't have /// access to the versions defined on the container. This function inserts /// all non-present container versions and decides which status and ident /// is the right fit based on the status neighbors. /// - /// This continous chain ensures that when generating code (tokens), each + /// This continuous chain ensures that when generating code (tokens), each /// field can lookup the status for a requested version. pub(crate) fn insert_container_versions(&mut self, versions: &Vec) { if let Some(chain) = &mut self.chain { @@ -206,7 +167,7 @@ impl VersionedField { (None, Some(_)) => chain.insert(version.inner, FieldStatus::NotPresent), (Some(status), None) => { let ident = match status { - FieldStatus::Added(ident) => ident, + FieldStatus::Added { ident, .. } => ident, FieldStatus::Renamed { _from: _, to } => to, FieldStatus::Deprecated { ident, note: _ } => ident, FieldStatus::NoChange(ident) => ident, @@ -217,7 +178,7 @@ impl VersionedField { } (Some(status), Some(_)) => { let ident = match status { - FieldStatus::Added(ident) => ident, + FieldStatus::Added { ident, .. } => ident, FieldStatus::Renamed { _from: _, to } => to, FieldStatus::NoChange(ident) => ident, _ => unreachable!(), @@ -230,13 +191,116 @@ impl VersionedField { } } } + + pub(crate) fn generate_for_struct( + &self, + container_version: &ContainerVersion, + ) -> Option { + match &self.chain { + Some(chain) => { + // Check if the provided container version is present in the map + // of actions. If it is, some action occurred in exactly that + // version and thus code is generated for that field based on + // the type of action. + // If not, the provided version has no action attached to it. + // The code generation then depends on the relation to other + // versions (with actions). + + let field_type = &self.inner.ty; + + match chain + .get(&container_version.inner) + .expect("internal error: chain must contain container version") + { + FieldStatus::Added { ident, .. } => Some(quote! { + pub #ident: #field_type, + }), + FieldStatus::Renamed { _from: _, to } => Some(quote! { + pub #to: #field_type, + }), + FieldStatus::Deprecated { + ident: field_ident, + note, + } => Some(quote! { + #[deprecated = #note] + pub #field_ident: #field_type, + }), + FieldStatus::NotPresent => None, + FieldStatus::NoChange(field_ident) => Some(quote! { + pub #field_ident: #field_type, + }), + } + } + None => { + // If there is no chain of field actions, the field is not + // versioned and code generation is straight forward. + // Unversioned fields are always included in versioned structs. + let field_ident = &self.inner.ident; + let field_type = &self.inner.ty; + + Some(quote! { + pub #field_ident: #field_type, + }) + } + } + } + + pub(crate) fn generate_for_from_impl( + &self, + version: &ContainerVersion, + next_version: &ContainerVersion, + from_ident: &Ident, + ) -> TokenStream { + match &self.chain { + Some(chain) => { + match ( + chain + .get(&version.inner) + .expect("internal error: chain must contain container version"), + chain + .get(&next_version.inner) + .expect("internal error: chain must contain container version"), + ) { + (_, FieldStatus::Added { ident, default_fn }) => quote! { + #ident: #default_fn(), + }, + (old, next) => { + let old_field_ident = old.get_ident().unwrap(); + let next_field_ident = next.get_ident().unwrap(); + + quote! { + #next_field_ident: #from_ident.#old_field_ident, + } + } + } + } + None => { + let field_ident = &self.inner.ident; + quote! { + #field_ident: #from_ident.#field_ident, + } + } + } + } } #[derive(Debug)] pub(crate) enum FieldStatus { - Added(Ident), + Added { ident: Ident, default_fn: Path }, Renamed { _from: Ident, to: Ident }, Deprecated { ident: Ident, note: String }, NoChange(Ident), NotPresent, } + +impl FieldStatus { + pub(crate) fn get_ident(&self) -> Option<&Ident> { + match &self { + FieldStatus::Added { ident, .. } => Some(ident), + FieldStatus::Renamed { _from, to } => Some(to), + FieldStatus::Deprecated { ident, .. } => Some(ident), + FieldStatus::NoChange(ident) => Some(ident), + FieldStatus::NotPresent => None, + } + } +} diff --git a/crates/stackable-versioned/src/gen/mod.rs b/crates/stackable-versioned-macros/src/gen/mod.rs similarity index 81% rename from crates/stackable-versioned/src/gen/mod.rs rename to crates/stackable-versioned-macros/src/gen/mod.rs index 0acb31699..bf64fa495 100644 --- a/crates/stackable-versioned/src/gen/mod.rs +++ b/crates/stackable-versioned-macros/src/gen/mod.rs @@ -1,11 +1,7 @@ use proc_macro2::TokenStream; -use quote::ToTokens; use syn::{spanned::Spanned, Data, DeriveInput, Error, Result}; -use crate::{ - attrs::container::ContainerAttributes, - gen::{version::ContainerVersion, vstruct::VersionedStruct}, -}; +use crate::{attrs::container::ContainerAttributes, gen::vstruct::VersionedStruct}; pub(crate) mod field; pub(crate) mod neighbors; @@ -26,7 +22,7 @@ pub(crate) mod vstruct; pub(crate) fn expand(attrs: ContainerAttributes, input: DeriveInput) -> Result { let expanded = match input.data { - Data::Struct(data) => VersionedStruct::new(input.ident, data, attrs)?.to_token_stream(), + Data::Struct(data) => VersionedStruct::new(input.ident, data, attrs)?.generate_tokens(), _ => { return Err(Error::new( input.span(), @@ -37,7 +33,3 @@ pub(crate) fn expand(attrs: ContainerAttributes, input: DeriveInput) -> Result Option; -} diff --git a/crates/stackable-versioned/src/gen/neighbors.rs b/crates/stackable-versioned-macros/src/gen/neighbors.rs similarity index 100% rename from crates/stackable-versioned/src/gen/neighbors.rs rename to crates/stackable-versioned-macros/src/gen/neighbors.rs diff --git a/crates/stackable-versioned/src/gen/version.rs b/crates/stackable-versioned-macros/src/gen/version.rs similarity index 56% rename from crates/stackable-versioned/src/gen/version.rs rename to crates/stackable-versioned-macros/src/gen/version.rs index ca5d7f0f5..e4375090e 100644 --- a/crates/stackable-versioned/src/gen/version.rs +++ b/crates/stackable-versioned-macros/src/gen/version.rs @@ -1,7 +1,10 @@ use k8s_version::Version; +use syn::Ident; -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct ContainerVersion { pub(crate) deprecated: bool, + pub(crate) skip_from: bool, pub(crate) inner: Version, + pub(crate) ident: Ident, } diff --git a/crates/stackable-versioned-macros/src/gen/vstruct.rs b/crates/stackable-versioned-macros/src/gen/vstruct.rs new file mode 100644 index 000000000..812a55b06 --- /dev/null +++ b/crates/stackable-versioned-macros/src/gen/vstruct.rs @@ -0,0 +1,189 @@ +use darling::FromField; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{DataStruct, Ident, Result}; + +use crate::{ + attrs::{container::ContainerAttributes, field::FieldAttributes}, + gen::{field::VersionedField, version::ContainerVersion}, +}; + +/// Stores individual versions of a single struct. Each version tracks field +/// actions, which describe if the field was added, renamed or deprecated in +/// that version. Fields which are not versioned, are included in every +/// version of the struct. +#[derive(Debug)] +pub(crate) struct VersionedStruct { + /// The ident, or name, of the versioned struct. + pub(crate) ident: Ident, + + /// The name of the struct used in `From` implementations. + pub(crate) from_ident: Ident, + + /// List of declared versions for this struct. Each version, except the + /// latest, generates a definition with appropriate fields. + pub(crate) versions: Vec, + + /// List of fields defined in the base struct. How, and if, a field should + /// generate code, is decided by the currently generated version. + pub(crate) fields: Vec, + + pub(crate) skip_from: bool, +} + +impl VersionedStruct { + pub(crate) fn new( + ident: Ident, + data: DataStruct, + attributes: ContainerAttributes, + ) -> Result { + // Convert the raw version attributes into a container version. + let versions = attributes + .versions + .iter() + .map(|v| ContainerVersion { + skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()), + ident: format_ident!("{version}", version = v.name.to_string()), + deprecated: v.deprecated.is_present(), + inner: v.name, + }) + .collect(); + + // Extract the field attributes for every field from the raw token + // stream and also validate that each field action version uses a + // version declared by the container attribute. + let mut fields = Vec::new(); + + for field in data.fields { + let attrs = FieldAttributes::from_field(&field)?; + attrs.validate_versions(&attributes, &field)?; + + let mut versioned_field = VersionedField::new(field, attrs)?; + versioned_field.insert_container_versions(&versions); + fields.push(versioned_field); + } + + let from_ident = format_ident!("__sv_{ident}", ident = ident.to_string().to_lowercase()); + + Ok(Self { + skip_from: attributes + .options + .skip + .map_or(false, |s| s.from.is_present()), + from_ident, + versions, + fields, + ident, + }) + } + + /// This generates the complete code for a single versioned struct. + /// + /// Internally, it will create a module for each declared version which + /// contains the struct with the appropriate fields. Additionally, it + /// generated `From` implementations, which enable conversion from an older + /// to a newer version. + pub(crate) fn generate_tokens(&self) -> TokenStream { + let mut token_stream = TokenStream::new(); + let mut versions = self.versions.iter().peekable(); + + while let Some(version) = versions.next() { + token_stream.extend(self.generate_version(version, versions.peek().copied())); + } + + token_stream + } + + fn generate_version( + &self, + version: &ContainerVersion, + next_version: Option<&ContainerVersion>, + ) -> TokenStream { + let mut token_stream = TokenStream::new(); + let struct_name = &self.ident; + + // Generate fields of the struct for `version`. + let fields = self.generate_struct_fields(version); + + // TODO (@Techassi): Make the generation of the module optional to + // enable the attribute macro to be applied to a module which + // generates versioned versions of all contained containers. + + let deprecated_attr = version.deprecated.then_some(quote! {#[deprecated]}); + let module_name = &version.ident; + + // Generate tokens for the module and the contained struct + token_stream.extend(quote! { + #[automatically_derived] + #deprecated_attr + pub mod #module_name { + pub struct #struct_name { + #fields + } + } + }); + + // Generate the From impl between this `version` and the next one. + if !self.skip_from && !version.skip_from { + token_stream.extend(self.generate_from_impl(version, next_version)); + } + + token_stream + } + + fn generate_struct_fields(&self, version: &ContainerVersion) -> TokenStream { + let mut token_stream = TokenStream::new(); + + for field in &self.fields { + token_stream.extend(field.generate_for_struct(version)); + } + + token_stream + } + + fn generate_from_impl( + &self, + version: &ContainerVersion, + next_version: Option<&ContainerVersion>, + ) -> TokenStream { + if let Some(next_version) = next_version { + let next_module_name = &next_version.ident; + let from_ident = &self.from_ident; + let module_name = &version.ident; + let struct_name = &self.ident; + + 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)] + impl From<#module_name::#struct_name> for #next_module_name::#struct_name { + fn from(#from_ident: #module_name::#struct_name) -> Self { + Self { + #fields + } + } + } + }; + } + + quote! {} + } + + fn generate_from_fields( + &self, + version: &ContainerVersion, + next_version: &ContainerVersion, + from_ident: &Ident, + ) -> TokenStream { + let mut token_stream = TokenStream::new(); + + for field in &self.fields { + token_stream.extend(field.generate_for_from_impl(version, next_version, from_ident)) + } + + token_stream + } +} diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs new file mode 100644 index 000000000..9c8ce122d --- /dev/null +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -0,0 +1,229 @@ +use darling::{ast::NestedMeta, FromMeta}; +use proc_macro::TokenStream; +use syn::{DeriveInput, Error}; + +use crate::attrs::container::ContainerAttributes; + +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 is prohibited by default. It is possible to opt-out +/// 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 +/// b1, b2, ..., bn and deprecated in +/// version _c_ must ensure _a < b1, b2, ..., +/// bn < c_. +/// - All field actions must use previously declared versions. Using versions +/// not present at the container level will result in an error. +/// +/// 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 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 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 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()) { + Ok(attrs) => match ContainerAttributes::from_list(&attrs) { + Ok(attrs) => attrs, + Err(err) => return err.write_errors().into(), + }, + Err(err) => return darling::Error::from(err).write_errors().into(), + }; + + // NOTE (@Techassi): For now, we can just use the DeriveInput type here, + // because we only support structs (and eventually enums) to be versioned. + // In the future - if we decide to support modules - this requires + // adjustments to also support modules. One possible solution might be to + // use an enum with two variants: Container(DeriveInput) and + // Module(ItemMod). + let input = syn::parse_macro_input!(input as DeriveInput); + + gen::expand(attrs, input) + .unwrap_or_else(Error::into_compile_error) + .into() +} diff --git a/crates/stackable-versioned/tests/basic.rs b/crates/stackable-versioned-macros/tests/basic.rs similarity index 93% rename from crates/stackable-versioned/tests/basic.rs rename to crates/stackable-versioned-macros/tests/basic.rs index b3dd86db8..ef8a1c55b 100644 --- a/crates/stackable-versioned/tests/basic.rs +++ b/crates/stackable-versioned-macros/tests/basic.rs @@ -1,4 +1,4 @@ -use stackable_versioned::versioned; +use stackable_versioned_macros::versioned; // To expand the generated code (for debugging and testing), it is recommended // to first change directory via `cd crates/stackable-versioned` and to then @@ -36,7 +36,7 @@ fn basic() { // The latest version (v3) #[allow(deprecated)] - let _ = Foo { + let _ = v3::Foo { deprecated_bar: 0, baz: false, }; diff --git a/crates/stackable-versioned-macros/tests/from.rs b/crates/stackable-versioned-macros/tests/from.rs new file mode 100644 index 000000000..e644e00d6 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/from.rs @@ -0,0 +1,88 @@ +use stackable_versioned_macros::versioned; + +#[allow(deprecated)] +#[test] +fn from() { + #[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, + } + + let foo_v1alpha1 = v1alpha1::Foo { baz: true }; + let foo_v1beta1 = v1beta1::Foo::from(foo_v1alpha1); + let foo_v1 = v1::Foo::from(foo_v1beta1); + + assert_eq!(foo_v1.deprecated_bar, 0); + assert!(foo_v1.baz); +} + +#[test] +fn from_custom_default_fn() { + #[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 + } + + let foo_v1alpha1 = v1alpha1::Foo { baz: true }; + let foo_v1beta1 = v1beta1::Foo::from(foo_v1alpha1); + + assert_eq!(foo_v1beta1.bar, 42); + assert!(foo_v1beta1.baz); +} + +#[test] +fn skip_from_all() { + #[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, + } +} + +#[test] +fn skip_from_version() { + #[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1", skip(from)), + version(name = "v1") + )] + pub struct Foo { + #[versioned( + added(since = "v1beta1"), + deprecated(since = "v1", note = "not needed") + )] + deprecated_bar: usize, + baz: bool, + } +} diff --git a/crates/stackable-versioned/CHANGELOG.md b/crates/stackable-versioned/CHANGELOG.md index c026f6e0a..869ae6ece 100644 --- a/crates/stackable-versioned/CHANGELOG.md +++ b/crates/stackable-versioned/CHANGELOG.md @@ -4,12 +4,16 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Changed + +- Add auto-generated `From for NEW` implementations ([#790]). - Change from derive macro to attribute macro to be able to generate code - _in place_ instead of _appending_ new code ([#CHANGEME]). + _in place_ instead of _appending_ new code ([#793]). - Improve action chain generation ([#784]). -[#784](https://github.com/stackabletech/operator-rs/pull/784) -[#CHANGEME](https://github.com/stackabletech/operator-rs/pull/CHANGEME) +[#784]: https://github.com/stackabletech/operator-rs/pull/784 +[#790]: https://github.com/stackabletech/operator-rs/pull/790 +[#793]: https://github.com/stackabletech/operator-rs/pull/793 ## [0.1.0] - 2024-05-08 diff --git a/crates/stackable-versioned/Cargo.toml b/crates/stackable-versioned/Cargo.toml index d2ff81b73..b1dd38b5c 100644 --- a/crates/stackable-versioned/Cargo.toml +++ b/crates/stackable-versioned/Cargo.toml @@ -6,16 +6,5 @@ license.workspace = true edition.workspace = true repository.workspace = true -[lib] -proc-macro = true - [dependencies] -k8s-version = { path = "../k8s-version", features = ["darling"] } - -darling.workspace = true -proc-macro2.workspace = true -syn.workspace = true -quote.workspace = true - -[dev-dependencies] -rstest.workspace = true +stackable-versioned-macros = { path = "../stackable-versioned-macros" } diff --git a/crates/stackable-versioned/README.md b/crates/stackable-versioned/README.md new file mode 100644 index 000000000..f603d335a --- /dev/null +++ b/crates/stackable-versioned/README.md @@ -0,0 +1,39 @@ + + +

+ Stackable Logo +

+ +

stackable-versioned

+ +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-green.svg)](https://docs.stackable.tech/home/stable/contributor/index.html) +[![Apache License 2.0](https://img.shields.io/badge/license-Apache--2.0-green)](./LICENSE) + +[Stackable Data Platform](https://stackable.tech/) | [Platform Docs](https://docs.stackable.tech/) | [Discussions](https://github.com/orgs/stackabletech/discussions) | [Discord](https://discord.gg/7kZ3BNnCAF) + +This crate enables versioning of structs (and enums in the future). It currently +supports Kubernetes API versions while declaring versions on a data type. This +will be extended to support SemVer versions, as well as custom version formats +in the future. + +```rust +use stackable_versioned::versioned; + +#[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + version(name = "v1"), + version(name = "v2"), + version(name = "v3") +)] +struct Foo { + /// My docs + #[versioned( + added(since = "v1alpha1"), + renamed(since = "v1beta1", from = "gau"), + deprecated(since = "v2", note = "not required anymore") + )] + deprecated_bar: usize, + baz: bool, +} +``` diff --git a/crates/stackable-versioned/src/gen/vstruct.rs b/crates/stackable-versioned/src/gen/vstruct.rs deleted file mode 100644 index 379d2ed3b..000000000 --- a/crates/stackable-versioned/src/gen/vstruct.rs +++ /dev/null @@ -1,107 +0,0 @@ -use darling::FromField; -use proc_macro2::TokenStream; -use quote::{format_ident, quote, ToTokens}; -use syn::{DataStruct, Ident, Result}; - -use crate::{ - attrs::{container::ContainerAttributes, field::FieldAttributes}, - gen::{field::VersionedField, version::ContainerVersion, ToTokensExt}, -}; - -/// Stores individual versions of a single struct. Each version tracks field -/// actions, which describe if the field was added, renamed or deprecated in -/// that version. Fields which are not versioned, are included in every -/// version of the struct. -#[derive(Debug)] -pub(crate) struct VersionedStruct { - /// The ident, or name, of the versioned struct. - pub(crate) ident: Ident, - - /// List of declared versions for this struct. Each version, except the - /// latest, generates a definition with appropriate fields. - pub(crate) versions: Vec, - - /// List of fields defined in the base struct. How, and if, a field should - /// generate code, is decided by the currently generated version. - pub(crate) fields: Vec, -} - -impl ToTokens for VersionedStruct { - fn to_tokens(&self, tokens: &mut TokenStream) { - let versions = self.versions.iter().peekable(); - let struct_name = &self.ident; - - for version in versions { - let mut fields = TokenStream::new(); - - for field in &self.fields { - fields.extend(field.to_tokens_for_version(version)); - } - - // TODO (@Techassi): Make the generation of the module optional to - // enable the attribute macro to be applied to a module which - // generates versioned versions of all contained containers. - - let deprecated_attr = version.deprecated.then_some(quote! {#[deprecated]}); - let module_name = format_ident!("{version}", version = version.inner.to_string()); - - tokens.extend(quote! { - #[automatically_derived] - #deprecated_attr - pub mod #module_name { - - pub struct #struct_name { - #fields - } - } - }); - } - - // Special handling for the last (and thus latest) version - let module_name = format_ident!( - "{version}", - version = self.versions.last().unwrap().inner.to_string() - ); - tokens.extend(quote! { - pub type #struct_name = #module_name::#struct_name; - }) - } -} - -impl VersionedStruct { - pub(crate) fn new( - ident: Ident, - data: DataStruct, - attributes: ContainerAttributes, - ) -> Result { - // Convert the raw version attributes into a container version. - let versions = attributes - .versions - .iter() - .map(|v| ContainerVersion { - deprecated: v.deprecated.is_present(), - inner: v.name, - }) - .collect(); - - // Extract the field attributes for every field from the raw token - // stream and also validate that each field action version uses a - // version declared by the container attribute. - let mut fields = Vec::new(); - - for field in data.fields { - let attrs = FieldAttributes::from_field(&field)?; - attrs.validate_versions(&attributes, &field)?; - - let mut versioned_field = VersionedField::new(field, attrs)?; - versioned_field.insert_container_versions(&versions); - fields.push(versioned_field); - } - - Ok(Self { - ident, - versions, - fields, - }) - } -} diff --git a/crates/stackable-versioned/src/lib.rs b/crates/stackable-versioned/src/lib.rs index 34d79cf2c..22925d547 100644 --- a/crates/stackable-versioned/src/lib.rs +++ b/crates/stackable-versioned/src/lib.rs @@ -1,32 +1,41 @@ -use darling::{ast::NestedMeta, FromMeta}; -use proc_macro::TokenStream; -use syn::{DeriveInput, Error}; +//! This crate enables versioning of structs (and enums in the future). It +//! currently supports Kubernetes API versions while declaring versions on a +//! data type. This will be extended to support SemVer versions, as well as +//! custom version formats in the future. +//! +//! ## Usage Guide +//! +//! ``` +//! use stackable_versioned::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, +//! } +//! ``` +//! +//! See [`versioned`] for an in-depth usage guide and a list of supported +//! parameters. -use crate::attrs::container::ContainerAttributes; +pub use stackable_versioned_macros::*; -mod attrs; -mod consts; -mod gen; +pub trait AsVersionStr { + const VERSION: &'static str; -#[proc_macro_attribute] -pub fn versioned(attrs: TokenStream, input: TokenStream) -> TokenStream { - let attrs = match NestedMeta::parse_meta_list(attrs.into()) { - Ok(attrs) => match ContainerAttributes::from_list(&attrs) { - Ok(attrs) => attrs, - Err(err) => return err.write_errors().into(), - }, - Err(err) => return darling::Error::from(err).write_errors().into(), - }; - - // NOTE (@Techassi): For now, we can just use the DeriveInput type here, - // because we only support structs (and eventually enums) to be versioned. - // In the future - if we decide to support modules - this requires - // adjustments to also support modules. One possible solution might be to - // use an enum with two variants: Container(DeriveInput) and - // Module(ItemMod). - let input = syn::parse_macro_input!(input as DeriveInput); - - gen::expand(attrs, input) - .unwrap_or_else(Error::into_compile_error) - .into() + fn as_version_str(&self) -> &'static str { + Self::VERSION + } }