diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a17d34e0c..4ad95fe4c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: '0' CARGO_PROFILE_DEV_DEBUG: '0' - RUST_TOOLCHAIN_VERSION: "1.81.0" + RUST_TOOLCHAIN_VERSION: "1.82.0" RUSTFLAGS: "-D warnings" RUSTDOCFLAGS: "-D warnings" RUST_LOG: "info" diff --git a/.github/workflows/pr_pre-commit.yaml b/.github/workflows/pr_pre-commit.yaml index f1bc0640f..6f2ceb8e0 100644 --- a/.github/workflows/pr_pre-commit.yaml +++ b/.github/workflows/pr_pre-commit.yaml @@ -6,7 +6,7 @@ on: env: CARGO_TERM_COLOR: always - RUST_TOOLCHAIN_VERSION: "1.81.0" + RUST_TOOLCHAIN_VERSION: "1.82.0" HADOLINT_VERSION: "v1.17.6" jobs: @@ -16,29 +16,10 @@ jobs: - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 0 - - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + - uses: stackabletech/actions/run-pre-commit@9bd13255f286e4b7a654617268abe1b2f37c3e0a # v0.3.0 with: - python-version: '3.12' - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_TOOLCHAIN_VERSION }} - components: rustfmt,clippy - - name: Setup Hadolint - shell: bash - run: | - set -euo pipefail - - LOCATION_DIR="$HOME/.local/bin" - LOCATION_BIN="$LOCATION_DIR/hadolint" - - SYSTEM=$(uname -s) - ARCH=$(uname -m) - - mkdir -p "$LOCATION_DIR" - curl -sL -o "${LOCATION_BIN}" "https://github.com/hadolint/hadolint/releases/download/${{ env.HADOLINT_VERSION }}/hadolint-$SYSTEM-$ARCH" - chmod 700 "${LOCATION_BIN}" - - echo "$LOCATION_DIR" >> "$GITHUB_PATH" - - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 - with: - extra_args: "--from-ref ${{ github.event.pull_request.base.sha }} --to-ref ${{ github.event.pull_request.head.sha }}" + rust: ${{ env.RUST_TOOLCHAIN_VERSION }} + # rust-src is required for trybuild stderr output comparison to work + # for our cases. + # See: https://github.com/dtolnay/trybuild/issues/236#issuecomment-1620950759 + rust-components: rustfmt,clippy,rust-src diff --git a/Cargo.lock b/Cargo.lock index 2df0bb9ec..bee50eaf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3152,7 +3152,6 @@ dependencies = [ "snafu 0.8.5", "stackable-shared", "stackable-versioned", - "strum", "syn 2.0.82", "trybuild", ] diff --git a/crates/stackable-versioned-macros/Cargo.toml b/crates/stackable-versioned-macros/Cargo.toml index b31ed9487..8f547ae16 100644 --- a/crates/stackable-versioned-macros/Cargo.toml +++ b/crates/stackable-versioned-macros/Cargo.toml @@ -38,7 +38,6 @@ itertools.workspace = true k8s-openapi = { workspace = true, optional = true } kube = { workspace = true, optional = true } proc-macro2.workspace = true -strum.workspace = true syn.workspace = true quote.workspace = true diff --git a/crates/stackable-versioned-macros/fixtures/inputs/default/module.rs b/crates/stackable-versioned-macros/fixtures/inputs/default/module.rs new file mode 100644 index 000000000..e16336ed7 --- /dev/null +++ b/crates/stackable-versioned-macros/fixtures/inputs/default/module.rs @@ -0,0 +1,23 @@ +#[versioned( + version(name = "v1alpha1"), + version(name = "v1"), + version(name = "v2alpha1") +)] +// --- +pub(crate) mod versioned { + pub struct Foo { + bar: usize, + + #[versioned(added(since = "v1"))] + baz: bool, + + #[versioned(deprecated(since = "v2alpha1"))] + deprecated_foo: String, + } + + // The following attribute is just to ensure no strange behavior occurs. + #[versioned] + pub struct Bar { + baz: String, + } +} diff --git a/crates/stackable-versioned-macros/fixtures/inputs/default/module_preserve.rs b/crates/stackable-versioned-macros/fixtures/inputs/default/module_preserve.rs new file mode 100644 index 000000000..84298e38c --- /dev/null +++ b/crates/stackable-versioned-macros/fixtures/inputs/default/module_preserve.rs @@ -0,0 +1,23 @@ +#[versioned( + version(name = "v1alpha1"), + version(name = "v1"), + version(name = "v2alpha1"), + preserve_module +)] +// --- +pub(crate) mod versioned { + pub struct Foo { + bar: usize, + + #[versioned(added(since = "v1"))] + baz: bool, + + #[versioned(deprecated(since = "v2alpha1"))] + deprecated_foo: String, + } + + #[versioned] + pub struct Bar { + baz: String, + } +} diff --git a/crates/stackable-versioned-macros/fixtures/inputs/k8s/skip.rs b/crates/stackable-versioned-macros/fixtures/inputs/k8s/skip.rs new file mode 100644 index 000000000..aa9f908fa --- /dev/null +++ b/crates/stackable-versioned-macros/fixtures/inputs/k8s/skip.rs @@ -0,0 +1,22 @@ +#[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + version(name = "v1"), + k8s( + group = "stackable.tech", + singular = "foo", + plural = "foos", + namespaced, + skip(merged_crd) + ) +)] +// --- +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +pub struct FooSpec { + #[versioned( + added(since = "v1beta1"), + changed(since = "v1", from_name = "bah", from_type = "u16") + )] + bar: usize, + baz: bool, +} diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_enum.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_enum.rs.snap index 32f2162a9..22a6eaa73 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_enum.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_enum.rs.snap @@ -21,7 +21,6 @@ mod v1alpha1 { #[automatically_derived] mod v1beta1 { use super::*; - /// ///Additional docs for this version which are purposefully long to ///show how manual line wrapping works. \ ///Multi-line docs are also supported, as per regular doc-comments. diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_struct.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_struct.rs.snap index 440311ca7..05b0bfd21 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_struct.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@attribute_struct.rs.snap @@ -20,7 +20,6 @@ mod v1alpha1 { #[automatically_derived] mod v1beta1 { use super::*; - /// ///Additional docs for this version which are purposefully long to ///show how manual line wrapping works. \ ///Multi-line docs are also supported, as per regular doc-comments. diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@basic_struct.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@basic_struct.rs.snap index 1de8b2805..98797e3e3 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@basic_struct.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@basic_struct.rs.snap @@ -15,7 +15,7 @@ pub(crate) mod v1alpha1 { } #[automatically_derived] #[allow(deprecated)] -impl From for v1beta1::Foo { +impl ::std::convert::From for v1beta1::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { Self { bar: __sv_foo.jjj.into(), @@ -33,7 +33,7 @@ pub(crate) mod v1beta1 { } } #[automatically_derived] -impl From for v1::Foo { +impl ::std::convert::From for v1::Foo { fn from(__sv_foo: v1beta1::Foo) -> Self { Self { bar: __sv_foo.bar.into(), @@ -52,7 +52,7 @@ pub(crate) mod v1 { } #[automatically_derived] #[allow(deprecated)] -impl From for v2::Foo { +impl ::std::convert::From for v2::Foo { fn from(__sv_foo: v1::Foo) -> Self { Self { deprecated_bar: __sv_foo.bar, @@ -72,7 +72,7 @@ pub(crate) mod v2 { } #[automatically_derived] #[allow(deprecated)] -impl From for v3::Foo { +impl ::std::convert::From for v3::Foo { fn from(__sv_foo: v2::Foo) -> Self { Self { deprecated_bar: __sv_foo.deprecated_bar, diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_enum.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_enum.rs.snap index 17503b80c..df0e35530 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_enum.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_enum.rs.snap @@ -12,7 +12,7 @@ mod v1alpha1 { } } #[automatically_derived] -impl From for v1beta1::Foo { +impl ::std::convert::From for v1beta1::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { match __sv_foo { v1alpha1::Foo::Bar => v1beta1::Foo::Bar, @@ -30,7 +30,7 @@ mod v1beta1 { } #[automatically_derived] #[allow(deprecated)] -impl From for v1::Foo { +impl ::std::convert::From for v1::Foo { fn from(__sv_foo: v1beta1::Foo) -> Self { match __sv_foo { v1beta1::Foo::Bar => v1::Foo::DeprecatedBar, @@ -49,7 +49,7 @@ mod v1 { } #[automatically_derived] #[allow(deprecated)] -impl From for v2::Foo { +impl ::std::convert::From for v2::Foo { fn from(__sv_foo: v1::Foo) -> Self { match __sv_foo { v1::Foo::DeprecatedBar => v2::Foo::DeprecatedBar, @@ -68,7 +68,7 @@ mod v2 { } #[automatically_derived] #[allow(deprecated)] -impl From for v3::Foo { +impl ::std::convert::From for v3::Foo { fn from(__sv_foo: v2::Foo) -> Self { match __sv_foo { v2::Foo::DeprecatedBar => v3::Foo::DeprecatedBar, diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_struct.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_struct.rs.snap index afb5354a1..94d92182d 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_struct.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@deprecate_struct.rs.snap @@ -12,7 +12,7 @@ mod v1alpha1 { } } #[automatically_derived] -impl From for v1beta1::Foo { +impl ::std::convert::From for v1beta1::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { Self { bar: __sv_foo.bar, @@ -30,7 +30,7 @@ mod v1beta1 { } #[automatically_derived] #[allow(deprecated)] -impl From for v1::Foo { +impl ::std::convert::From for v1::Foo { fn from(__sv_foo: v1beta1::Foo) -> Self { Self { deprecated_bar: __sv_foo.bar, @@ -49,7 +49,7 @@ mod v1 { } #[automatically_derived] #[allow(deprecated)] -impl From for v2::Foo { +impl ::std::convert::From for v2::Foo { fn from(__sv_foo: v1::Foo) -> Self { Self { deprecated_bar: __sv_foo.deprecated_bar, @@ -68,7 +68,7 @@ mod v2 { } #[automatically_derived] #[allow(deprecated)] -impl From for v3::Foo { +impl ::std::convert::From for v3::Foo { fn from(__sv_foo: v2::Foo) -> Self { Self { deprecated_bar: __sv_foo.deprecated_bar, diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@enum_data_simple.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@enum_data_simple.rs.snap index 642b6560a..d945ef961 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@enum_data_simple.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@enum_data_simple.rs.snap @@ -12,7 +12,7 @@ mod v1alpha1 { } } #[automatically_derived] -impl From for v1alpha2::Foo { +impl ::std::convert::From for v1alpha2::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { match __sv_foo { v1alpha1::Foo::Foo => v1alpha2::Foo::Foo, diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@module.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@module.rs.snap new file mode 100644 index 000000000..50a7a308c --- /dev/null +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@module.rs.snap @@ -0,0 +1,74 @@ +--- +source: crates/stackable-versioned-macros/src/lib.rs +expression: formatted +input_file: crates/stackable-versioned-macros/fixtures/inputs/default/module.rs +--- +#[automatically_derived] +pub(crate) mod v1alpha1 { + use super::*; + pub struct Foo { + pub bar: usize, + pub foo: String, + } + pub struct Bar { + pub baz: String, + } +} +#[automatically_derived] +impl ::std::convert::From for v1::Foo { + fn from(__sv_foo: v1alpha1::Foo) -> Self { + Self { + bar: __sv_foo.bar, + baz: ::std::default::Default::default(), + foo: __sv_foo.foo, + } + } +} +#[automatically_derived] +impl ::std::convert::From for v1::Bar { + fn from(__sv_bar: v1alpha1::Bar) -> Self { + Self { baz: __sv_bar.baz } + } +} +#[automatically_derived] +pub(crate) mod v1 { + use super::*; + pub struct Foo { + pub bar: usize, + pub baz: bool, + pub foo: String, + } + pub struct Bar { + pub baz: String, + } +} +#[automatically_derived] +#[allow(deprecated)] +impl ::std::convert::From for v2alpha1::Foo { + fn from(__sv_foo: v1::Foo) -> Self { + Self { + bar: __sv_foo.bar, + baz: __sv_foo.baz, + deprecated_foo: __sv_foo.foo, + } + } +} +#[automatically_derived] +impl ::std::convert::From for v2alpha1::Bar { + fn from(__sv_bar: v1::Bar) -> Self { + Self { baz: __sv_bar.baz } + } +} +#[automatically_derived] +pub(crate) mod v2alpha1 { + use super::*; + pub struct Foo { + pub bar: usize, + pub baz: bool, + #[deprecated] + pub deprecated_foo: String, + } + pub struct Bar { + pub baz: String, + } +} diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@module_preserve.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@module_preserve.rs.snap new file mode 100644 index 000000000..1570369a5 --- /dev/null +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@module_preserve.rs.snap @@ -0,0 +1,70 @@ +--- +source: crates/stackable-versioned-macros/src/lib.rs +expression: formatted +input_file: crates/stackable-versioned-macros/fixtures/inputs/default/module_preserve.rs +--- +#[automatically_derived] +pub(crate) mod versioned { + pub mod v1alpha1 { + use super::*; + pub struct Foo { + pub bar: usize, + pub foo: String, + } + pub struct Bar { + pub baz: String, + } + } + impl ::std::convert::From for v1::Foo { + fn from(__sv_foo: v1alpha1::Foo) -> Self { + Self { + bar: __sv_foo.bar, + baz: ::std::default::Default::default(), + foo: __sv_foo.foo, + } + } + } + impl ::std::convert::From for v1::Bar { + fn from(__sv_bar: v1alpha1::Bar) -> Self { + Self { baz: __sv_bar.baz } + } + } + pub mod v1 { + use super::*; + pub struct Foo { + pub bar: usize, + pub baz: bool, + pub foo: String, + } + pub struct Bar { + pub baz: String, + } + } + #[allow(deprecated)] + impl ::std::convert::From for v2alpha1::Foo { + fn from(__sv_foo: v1::Foo) -> Self { + Self { + bar: __sv_foo.bar, + baz: __sv_foo.baz, + deprecated_foo: __sv_foo.foo, + } + } + } + impl ::std::convert::From for v2alpha1::Bar { + fn from(__sv_bar: v1::Bar) -> Self { + Self { baz: __sv_bar.baz } + } + } + pub mod v2alpha1 { + use super::*; + pub struct Foo { + pub bar: usize, + pub baz: bool, + #[deprecated] + pub deprecated_foo: String, + } + pub struct Bar { + pub baz: String, + } + } +} diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@rename.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@rename.rs.snap index 6e1bc3e84..ed4443c86 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@rename.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@rename.rs.snap @@ -12,7 +12,7 @@ mod v1alpha1 { } } #[automatically_derived] -impl From for v1beta1::Foo { +impl ::std::convert::From for v1beta1::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { Self { bar: __sv_foo.bat, @@ -29,7 +29,7 @@ mod v1beta1 { } } #[automatically_derived] -impl From for v1::Foo { +impl ::std::convert::From for v1::Foo { fn from(__sv_foo: v1beta1::Foo) -> Self { Self { bar: __sv_foo.bar, diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@skip_from_field.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@skip_from_field.rs.snap index 1b40c6704..ccddd846e 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@skip_from_field.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__default_snapshots@skip_from_field.rs.snap @@ -11,7 +11,7 @@ pub mod v1alpha1 { } } #[automatically_derived] -impl From for v1beta1::Foo { +impl ::std::convert::From for v1beta1::Foo { fn from(__sv_foo: v1alpha1::Foo) -> Self { Self { bar: ::std::default::Default::default(), diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@basic.rs.snap similarity index 94% rename from crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots.snap rename to crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@basic.rs.snap index df79bb1da..694454b2c 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@basic.rs.snap @@ -21,7 +21,7 @@ pub mod v1alpha1 { } } #[automatically_derived] -impl From for v1beta1::FooSpec { +impl ::std::convert::From for v1beta1::FooSpec { fn from(__sv_foospec: v1alpha1::FooSpec) -> Self { Self { bah: ::std::default::Default::default(), @@ -48,7 +48,7 @@ pub mod v1beta1 { } } #[automatically_derived] -impl From for v1::FooSpec { +impl ::std::convert::From for v1::FooSpec { fn from(__sv_foospec: v1beta1::FooSpec) -> Self { Self { bar: __sv_foospec.bah.into(), @@ -87,9 +87,9 @@ impl ::std::fmt::Display for Foo { f: &mut ::std::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { match self { - Foo::V1Alpha1 => f.write_str("v1alpha1"), - Foo::V1Beta1 => f.write_str("v1beta1"), - Foo::V1 => f.write_str("v1"), + Self::V1Alpha1 => f.write_str("v1alpha1"), + Self::V1Beta1 => f.write_str("v1beta1"), + Self::V1 => f.write_str("v1"), } } } diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@skip.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@skip.rs.snap new file mode 100644 index 000000000..1b81fbf41 --- /dev/null +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@skip.rs.snap @@ -0,0 +1,76 @@ +--- +source: crates/stackable-versioned-macros/src/lib.rs +expression: formatted +input_file: crates/stackable-versioned-macros/fixtures/inputs/k8s/skip.rs +--- +#[automatically_derived] +pub mod v1alpha1 { + use super::*; + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] + #[derive(::kube::CustomResource)] + #[kube( + group = "stackable.tech", + version = "v1alpha1", + kind = "Foo", + singular = "foo", + plural = "foos", + namespaced + )] + pub struct FooSpec { + pub baz: bool, + } +} +#[automatically_derived] +impl ::std::convert::From for v1beta1::FooSpec { + fn from(__sv_foospec: v1alpha1::FooSpec) -> Self { + Self { + bah: ::std::default::Default::default(), + baz: __sv_foospec.baz, + } + } +} +#[automatically_derived] +pub mod v1beta1 { + use super::*; + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] + #[derive(::kube::CustomResource)] + #[kube( + group = "stackable.tech", + version = "v1beta1", + kind = "Foo", + singular = "foo", + plural = "foos", + namespaced + )] + pub struct FooSpec { + pub bah: u16, + pub baz: bool, + } +} +#[automatically_derived] +impl ::std::convert::From for v1::FooSpec { + fn from(__sv_foospec: v1beta1::FooSpec) -> Self { + Self { + bar: __sv_foospec.bah.into(), + baz: __sv_foospec.baz, + } + } +} +#[automatically_derived] +pub mod v1 { + use super::*; + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] + #[derive(::kube::CustomResource)] + #[kube( + group = "stackable.tech", + version = "v1", + kind = "Foo", + singular = "foo", + plural = "foos", + namespaced + )] + pub struct FooSpec { + pub bar: usize, + pub baz: bool, + } +} diff --git a/crates/stackable-versioned-macros/src/attrs/common.rs b/crates/stackable-versioned-macros/src/attrs/common.rs new file mode 100644 index 000000000..c70426002 --- /dev/null +++ b/crates/stackable-versioned-macros/src/attrs/common.rs @@ -0,0 +1,94 @@ +use darling::{ + util::{Flag, Override, SpannedValue}, + Error, FromMeta, Result, +}; +use itertools::Itertools; +use k8s_version::Version; + +#[derive(Debug, FromMeta)] +#[darling(and_then = CommonRootArguments::validate)] +pub(crate) struct CommonRootArguments { + #[darling(default)] + pub(crate) options: RootOptions, + + #[darling(multiple, rename = "version")] + pub(crate) versions: SpannedValue>, +} + +impl CommonRootArguments { + fn validate(mut self) -> Result { + let mut errors = Error::accumulator(); + + if self.versions.is_empty() { + errors.push( + Error::custom("at least one or more `version`s must be defined") + .with_span(&self.versions.span()), + ); + } + + let is_sorted = self.versions.iter().is_sorted_by_key(|v| v.name); + + // It needs to be sorted, even though the definition could be unsorted + // (if allow_unsorted is set). + self.versions.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); + + if !self.options.allow_unsorted.is_present() && !is_sorted { + let versions = self.versions.iter().map(|v| v.name).join(", "); + + errors.push(Error::custom(format!( + "versions must be defined in ascending order: {versions}", + ))); + } + + let duplicate_versions: Vec<_> = self + .versions + .iter() + .duplicates_by(|v| v.name) + .map(|v| v.name) + .collect(); + + if !duplicate_versions.is_empty() { + let versions = duplicate_versions.iter().join(", "); + + errors.push(Error::custom(format!( + "contains duplicate versions: {versions}", + ))); + } + + errors.finish_with(self) + } +} + +#[derive(Clone, Debug, Default, FromMeta)] +pub(crate) struct RootOptions { + pub(crate) allow_unsorted: Flag, + pub(crate) skip: Option, +} + +/// This struct contains supported version arguments. +/// +/// Supported arguments are: +/// +/// - `name` of the version, like `v1alpha1`. +/// - `deprecated` flag to mark that version as deprecated. +/// - `skip` option to skip generating various pieces of code. +/// - `doc` option to add version-specific documentation. +#[derive(Clone, Debug, FromMeta)] +pub(crate) struct VersionArguments { + pub(crate) deprecated: Option>, + pub(crate) name: Version, + pub(crate) skip: Option, + pub(crate) doc: Option, +} + +/// This struct contains supported common skip arguments. +/// +/// Supported arguments are: +/// +/// - `from` flag, which skips generating [`From`] implementations when provided. +#[derive(Clone, Debug, Default, FromMeta)] +pub(crate) struct SkipArguments { + /// Whether the [`From`] implementation generation should be skipped for all versions of this + /// container. + pub(crate) from: Flag, +} diff --git a/crates/stackable-versioned-macros/src/attrs/common/container.rs b/crates/stackable-versioned-macros/src/attrs/common/container.rs deleted file mode 100644 index 434c9e1ab..000000000 --- a/crates/stackable-versioned-macros/src/attrs/common/container.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::{cmp::Ordering, ops::Deref}; - -use darling::{ - util::{Flag, SpannedValue}, - Error, FromMeta, Result, -}; -use itertools::Itertools; -use k8s_version::Version; - -/// This struct contains supported container attributes. -/// -/// Currently supported attributes are: -/// -/// - `version`, which can occur one or more times. See [`VersionAttributes`]. -/// - `options`, which allow further customization of the generated code. -/// See [`ContainerAttributes`]. -#[derive(Debug, FromMeta)] -#[darling(and_then = ContainerAttributes::validate)] -pub(crate) struct ContainerAttributes { - #[darling(multiple, rename = "version")] - pub(crate) versions: SpannedValue>, - - #[darling(rename = "k8s")] - pub(crate) kubernetes_attrs: Option, - - #[darling(default, rename = "options")] - pub(crate) common_option_attrs: OptionAttributes, -} - -impl ContainerAttributes { - fn validate(mut self) -> Result { - // Most of the validation for individual version strings is done by the - // k8s-version crate. That's why the code below only checks that at - // least one version is defined, they are defined in order (to ensure - // code consistency) and that all declared versions are unique. - - // If there are no versions defined, the derive macro errors out. There - // should be at least one version if the derive macro is used. - if self.versions.is_empty() { - return Err(Error::custom( - "attribute macro `#[versioned()]` must contain at least one `version`", - ) - .with_span(&self.versions.span())); - } - - // NOTE (@Techassi): Do we even want to allow to opt-out of this? - - // Ensure that versions are defined in sorted (ascending) order to keep - // code consistent. - if !self.common_option_attrs.allow_unsorted.is_present() { - let original = self.versions.deref().clone(); - self.versions - .sort_by(|lhs, rhs| lhs.name.partial_cmp(&rhs.name).unwrap_or(Ordering::Equal)); - - for (index, version) in original.iter().enumerate() { - if version.name - == self - .versions - .get(index) - .expect("internal error: version at that index must exist") - .name - { - continue; - } - - return Err(Error::custom(format!( - "versions in `#[versioned()]` must be defined in ascending order (version `{name}` is misplaced)", - name = version.name - ))); - } - } - - // 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. - let duplicate_versions = self - .versions - .iter() - .duplicates_by(|e| e.name) - .map(|e| e.name) - .join(", "); - - if !duplicate_versions.is_empty() { - return Err(Error::custom(format!( - "attribute macro `#[versioned()]` contains duplicate versions: {duplicate_versions}", - )) - .with_span(&self.versions.span())); - } - - // Ensure that the 'k8s' feature is enabled when the 'k8s()' - // attribute is used. - if self.kubernetes_attrs.is_some() && cfg!(not(feature = "k8s")) { - return Err(Error::custom( - "the `#[versioned(k8s())]` attribute can only be used when the `k8s` feature is enabled", - )); - } - - Ok(self) - } -} - -/// This struct contains supported version options. -/// -/// Supported options are: -/// -/// - `name` of the version, like `v1alpha1`. -/// - `deprecated` flag to mark that version as deprecated. -/// - `skip` option to skip generating various pieces of code. -/// - `doc` option to add version-specific documentation. -#[derive(Clone, Debug, FromMeta)] -pub(crate) struct VersionAttributes { - pub(crate) deprecated: Flag, - pub(crate) name: Version, - pub(crate) skip: Option, - pub(crate) doc: Option, -} - -/// This struct contains supported option attributes. -/// -/// Supported attributes are: -/// -/// - `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 OptionAttributes { - pub(crate) allow_unsorted: Flag, - pub(crate) skip: Option, -} - -/// This struct contains supported Kubernetes attributes. -/// -/// Supported attributes are: -/// -/// - `skip`, which controls skipping parts of the generation. -/// - `kind`, which allows overwriting the kind field of the CRD. This defaults -/// to the struct name (without the 'Spec' suffix). -/// - `group`, which sets the CRD group, usually the domain of the company. -#[derive(Clone, Debug, FromMeta)] -pub(crate) struct KubernetesAttributes { - pub(crate) skip: Option, - pub(crate) singular: Option, - pub(crate) plural: Option, - pub(crate) kind: Option, - pub(crate) namespaced: Flag, - pub(crate) group: String, -} - -/// This struct contains supported kubernetes skip attributes. -/// -/// Supported attributes are: -/// -/// - `merged_crd` flag, which skips generating the `crd()` and `merged_crd()` -/// functions are generated. -#[derive(Clone, Debug, FromMeta)] -pub(crate) struct KubernetesSkipAttributes { - /// Whether the `crd()` and `merged_crd()` generation should be skipped for - /// this container. - pub(crate) merged_crd: Flag, -} - -/// This struct contains supported common skip attributes. -/// -/// Supported attributes are: -/// -/// - `from` flag, which skips generating [`From`] implementations when provided. -#[derive(Clone, Debug, Default, FromMeta)] -pub(crate) struct CommonSkipAttributes { - /// Whether the [`From`] implementation generation should be skipped for all - /// versions of this container. - pub(crate) from: Flag, -} diff --git a/crates/stackable-versioned-macros/src/attrs/common/item.rs b/crates/stackable-versioned-macros/src/attrs/common/item.rs deleted file mode 100644 index 42b6ac953..000000000 --- a/crates/stackable-versioned-macros/src/attrs/common/item.rs +++ /dev/null @@ -1,376 +0,0 @@ -use darling::{util::SpannedValue, Error, FromMeta}; -use k8s_version::Version; -use proc_macro2::Span; -use syn::{spanned::Spanned, Attribute, Ident, Path, Type}; - -use crate::{ - attrs::common::ContainerAttributes, - codegen::common::Attributes, - consts::{DEPRECATED_FIELD_PREFIX, DEPRECATED_VARIANT_PREFIX}, -}; - -/// This trait helps to unify attribute validation for both field and variant -/// attributes. -/// -/// This trait is implemented using a blanket implementation on types -/// `T: Attributes`. The [`Attributes`] trait allows access to the common -/// attributes shared across field and variant attributes. -pub(crate) trait ValidateVersions -where - I: Spanned, -{ - /// Validates that each field or variant action version is present in the - /// declared container versions. - fn validate_versions( - &self, - container_attrs: &ContainerAttributes, - item: &I, - ) -> Result<(), darling::Error>; -} - -impl ValidateVersions for T -where - T: Attributes, - I: Spanned, -{ - fn validate_versions( - &self, - container_attrs: &ContainerAttributes, - item: &I, - ) -> Result<(), darling::Error> { - // NOTE (@Techassi): Can we maybe optimize this a little? - - let mut errors = Error::accumulator(); - - if let Some(added) = &self.common_attributes().added { - if !container_attrs - .versions - .iter() - .any(|v| v.name == *added.since) - { - errors.push(Error::custom( - "variant action `added` uses version which was not declared via #[versioned(version)]") - .with_span(item) - ); - } - } - - for change in &*self.common_attributes().changes { - if !container_attrs - .versions - .iter() - .any(|v| v.name == *change.since) - { - errors.push( - Error::custom("variant action `changed` uses version which was not declared via #[versioned(version)]") - .with_span(item) - ); - } - } - - if let Some(deprecated) = &self.common_attributes().deprecated { - if !container_attrs - .versions - .iter() - .any(|v| v.name == *deprecated.since) - { - errors.push(Error::custom( - "variant action `deprecated` uses version which was not declared via #[versioned(version)]") - .with_span(item) - ); - } - } - - errors.finish()?; - Ok(()) - } -} - -// NOTE (@Techassi): It might be possible (but is it required) to move this -// functionality into a shared trait, which knows what type of item 'Self' is. - -/// This enum is used to run different validation based on the type of item. -#[derive(Debug, strum::Display)] -#[strum(serialize_all = "lowercase")] -pub(crate) enum ItemType { - Field, - Variant, -} - -// TODO (@Techassi): Shower thought: Track actions as a Vec of an ActionAttribute -// enum and implement Ord on it. This would allow us to order the items by type -// of action (added < changed < deprecated) as well as sort changed action to -// each other by specified version (which already implements Ord) - -/// These attributes are meant to be used in super structs, which add -/// [`Field`](syn::Field) or [`Variant`](syn::Variant) specific attributes via -/// darling's flatten feature. This struct only provides shared attributes. -/// -/// ### Shared Item Rules -/// -/// - An item can only ever be added once at most. An item not marked as 'added' -/// is part of the container in every version until changed or deprecated. -/// - An item can be changed many times. That's why changes are stored in a -/// [`Vec`]. -/// - An item can only be deprecated once. A field or variant not marked as -/// 'deprecated' will be included up until the latest version. -#[derive(Debug, FromMeta)] -pub(crate) struct ItemAttributes { - /// This parses the `added` attribute on items (fields or variants). It can - /// only be present at most once. - pub(crate) added: Option, - - /// This parses the `changed` attribute on items (fields or variants). It - /// can be present 0..n times. - #[darling(multiple, rename = "changed")] - pub(crate) changes: Vec, - - /// This parses the `deprecated` attribute on items (fields or variants). It - /// can only be present at most once. - pub(crate) deprecated: Option, -} - -impl ItemAttributes { - pub(crate) fn validate( - &self, - item_ident: &Ident, - item_type: &ItemType, - item_attrs: &Vec, - ) -> Result<(), Error> { - // NOTE (@Techassi): This associated function is NOT called by darling's - // and_then attribute, but instead by the wrapper, FieldAttributes and - // VariantAttributes. - - let mut errors = Error::accumulator(); - - // Semantic validation - errors.handle(self.validate_action_combinations(item_ident, item_type)); - errors.handle(self.validate_action_order(item_ident, item_type)); - errors.handle(self.validate_item_name(item_ident, item_type)); - errors.handle(self.validate_changed_item_name(item_type)); - errors.handle(self.validate_item_attributes(item_attrs)); - - // TODO (@Techassi): Add hint if a field or variant is added in the - // first version that it might be clever to remove the 'added' - // attribute. - - errors.finish()?; - - Ok(()) - } - - /// This associated function is called by the top-level validation function - /// and validates that each item uses a valid combination of actions. - /// Invalid combinations are: - /// - /// - `added` and `deprecated` using the same version: A field or variant - /// cannot be marked as added in a particular version and then marked as - /// deprecated immediately after. Fields and variants must be included for - /// at least one version before being marked deprecated. - /// - `added` and `changed` using the same version: The same reasoning from - /// above applies here as well. Fields and variants must be included for - /// at least one version before being changed. - /// - `changed` and `deprecated` using the same version: Again, the same - /// rules from above apply here as well. - fn validate_action_combinations( - &self, - item_ident: &Ident, - item_type: &ItemType, - ) -> Result<(), Error> { - match (&self.added, &self.changes, &self.deprecated) { - (Some(added), _, Some(deprecated)) if *added.since == *deprecated.since => { - Err(Error::custom(format!( - "{item_type} cannot be marked as `added` and `deprecated` in the same version" - )) - .with_span(item_ident)) - } - (Some(added), changed, _) if changed.iter().any(|r| *r.since == *added.since) => { - Err(Error::custom(format!( - "{item_type} cannot be marked as `added` and `changed` in the same version" - )) - .with_span(item_ident)) - } - (_, changed, Some(deprecated)) - if changed.iter().any(|r| *r.since == *deprecated.since) => - { - Err(Error::custom( - format!("{item_type} cannot be marked as `deprecated` and `changed` in the same version"), - ) - .with_span(item_ident)) - } - _ => Ok(()), - } - } - - /// This associated function is called by the top-level validation function - /// and validates that actions use a chronologically sound chain of - /// versions. - /// - /// The following rules apply: - /// - /// - `deprecated` must use a greater version than `added`: This function - /// ensures that these versions are chronologically sound, that means, - /// that the version of the deprecated action must be greater than the - /// version of the added action. - /// - All `changed` actions must use a greater version than `added` but a - /// lesser version than `deprecated`. - fn validate_action_order(&self, item_ident: &Ident, item_type: &ItemType) -> Result<(), Error> { - let added_version = self.added.as_ref().map(|a| *a.since); - let deprecated_version = self.deprecated.as_ref().map(|d| *d.since); - - // 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 { - return Err(Error::custom(format!( - "{item_type} was marked as `added` in version `{added_version}` while being marked as `deprecated` in an earlier version `{deprecated_version}`" - )).with_span(item_ident)); - } - } - - // Now, iterate over all changes and ensure that their versions are - // between the added and deprecated version. - if !self.changes.iter().all(|r| { - added_version.map_or(true, |a| a < *r.since) - && deprecated_version.map_or(true, |d| d > *r.since) - }) { - return Err(Error::custom( - "all changes must use versions higher than `added` and lower than `deprecated`", - ) - .with_span(item_ident)); - } - - Ok(()) - } - - /// This associated function is called by the top-level validation function - /// and validates that items use correct names depending on attached - /// actions. - /// - /// The following naming rules apply: - /// - /// - Fields or variants marked as deprecated need to include the - /// deprecation prefix in their name. The prefix must not be included for - /// fields or variants which are not deprecated. - fn validate_item_name(&self, item_ident: &Ident, item_type: &ItemType) -> Result<(), Error> { - let prefix = match item_type { - ItemType::Field => DEPRECATED_FIELD_PREFIX, - ItemType::Variant => DEPRECATED_VARIANT_PREFIX, - }; - - let starts_with_deprecated = item_ident.to_string().starts_with(prefix); - - if self.deprecated.is_some() && !starts_with_deprecated { - return Err(Error::custom( - format!("{item_type} was marked as `deprecated` and thus must include the `{prefix}` prefix in its name") - ).with_span(item_ident)); - } - - if self.deprecated.is_none() && starts_with_deprecated { - return Err(Error::custom( - format!("{item_type} includes the `{prefix}` prefix in its name but is not marked as `deprecated`") - ).with_span(item_ident)); - } - - Ok(()) - } - - /// This associated function is called by the top-level validation function - /// and validates that disallowed item attributes are not used. - /// - /// The following naming rules apply: - /// - /// - `deprecated` must not be set on items. Instead, use the `deprecated()` - /// action of the `#[versioned()]` macro. - fn validate_item_attributes(&self, item_attrs: &Vec) -> Result<(), Error> { - for attr in item_attrs { - for segment in &attr.path().segments { - if segment.ident == "deprecated" { - return Err(Error::custom("deprecation must be done using #[versioned(deprecated(since = \"VERSION\"))]") - .with_span(&attr.span())); - } - } - } - Ok(()) - } - - /// This associated function is called by the top-level validation function - /// and validates that parameters provided to the `changed` actions are - /// valid. - fn validate_changed_item_name(&self, item_type: &ItemType) -> Result<(), Error> { - let prefix = match item_type { - ItemType::Field => DEPRECATED_FIELD_PREFIX, - ItemType::Variant => DEPRECATED_VARIANT_PREFIX, - }; - - let mut errors = Error::accumulator(); - - // This ensures that `from_name` doesn't include the deprecation prefix. - for change in &self.changes { - if let Some(from_name) = change.from_name.as_ref() { - if from_name.starts_with(prefix) { - errors.push( - Error::custom(format!( - "the previous {item_type} name must not start with the deprecation prefix" - )) - .with_span(&from_name.span()), - ); - } - } - } - - errors.finish() - } -} - -// TODO (@Techassi): Add validation for when default_fn is "" (empty path). -/// For the added() action -/// -/// Example usage: -/// - `added(since = "...")` -/// - `added(since = "...", default_fn = "custom_fn")` -#[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") - .expect("internal error: path must parse"), - Span::call_site(), - ) -} - -// TODO (@Techassi): Add validation for when from_name AND from_type are both -// none => is this action needed in the first place? -// TODO (@Techassi): Add validation that the from_name mustn't include the -// deprecated prefix. -/// For the changed() action -/// -/// Example usage: -/// - `changed(since = "...", from_name = "...")` -/// - `changed(since = "...", from_name = "..." from_type="...")` -#[derive(Clone, Debug, FromMeta)] -pub(crate) struct ChangedAttributes { - pub(crate) since: SpannedValue, - pub(crate) from_name: Option>, - - pub(crate) from_type: Option>, -} - -/// For the deprecated() action -/// -/// Example usage: -/// - `deprecated(since = "...")` -/// - `deprecated(since = "...", note = "...")` -#[derive(Clone, Debug, FromMeta)] -pub(crate) struct DeprecatedAttributes { - pub(crate) since: SpannedValue, - pub(crate) note: Option>, -} diff --git a/crates/stackable-versioned-macros/src/attrs/common/mod.rs b/crates/stackable-versioned-macros/src/attrs/common/mod.rs deleted file mode 100644 index 175e8eed5..000000000 --- a/crates/stackable-versioned-macros/src/attrs/common/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod container; -mod item; - -pub(crate) use container::*; -pub(crate) use item::*; diff --git a/crates/stackable-versioned-macros/src/attrs/container.rs b/crates/stackable-versioned-macros/src/attrs/container.rs new file mode 100644 index 000000000..f9af53c25 --- /dev/null +++ b/crates/stackable-versioned-macros/src/attrs/container.rs @@ -0,0 +1,54 @@ +use darling::{Error, FromAttributes, FromMeta, Result}; + +use crate::attrs::{ + common::{CommonRootArguments, SkipArguments}, + k8s::KubernetesArguments, +}; + +#[derive(Debug, FromMeta)] +#[darling(and_then = StandaloneContainerAttributes::validate)] +pub(crate) struct StandaloneContainerAttributes { + #[darling(rename = "k8s")] + pub(crate) kubernetes_arguments: Option, + + #[darling(flatten)] + pub(crate) common_root_arguments: CommonRootArguments, +} + +impl StandaloneContainerAttributes { + fn validate(self) -> Result { + if self.kubernetes_arguments.is_some() && cfg!(not(feature = "k8s")) { + return Err(Error::custom("the `#[versioned(k8s())]` attribute can only be used when the `k8s` feature is enabled")); + } + + Ok(self) + } +} + +#[derive(Debug, FromAttributes)] +#[darling( + attributes(versioned), + and_then = NestedContainerAttributes::validate +)] +pub(crate) struct NestedContainerAttributes { + #[darling(rename = "k8s")] + pub(crate) kubernetes_arguments: Option, + + #[darling(default)] + pub(crate) options: NestedContainerOptionArguments, +} + +impl NestedContainerAttributes { + fn validate(self) -> Result { + if self.kubernetes_arguments.is_some() && cfg!(not(feature = "k8s")) { + return Err(Error::custom("the `#[versioned(k8s())]` attribute can only be used when the `k8s` feature is enabled")); + } + + Ok(self) + } +} + +#[derive(Debug, Default, FromMeta)] +pub(crate) struct NestedContainerOptionArguments { + pub(crate) skip: Option, +} diff --git a/crates/stackable-versioned-macros/src/attrs/field.rs b/crates/stackable-versioned-macros/src/attrs/item/field.rs similarity index 70% rename from crates/stackable-versioned-macros/src/attrs/field.rs rename to crates/stackable-versioned-macros/src/attrs/item/field.rs index d5c9aa9ff..bee008fd2 100644 --- a/crates/stackable-versioned-macros/src/attrs/field.rs +++ b/crates/stackable-versioned-macros/src/attrs/item/field.rs @@ -1,7 +1,7 @@ -use darling::{Error, FromField}; +use darling::{FromField, Result}; use syn::{Attribute, Ident}; -use crate::attrs::common::{ItemAttributes, ItemType}; +use crate::{attrs::item::CommonItemAttributes, codegen::VersionDefinition, utils::FieldIdent}; /// This struct describes all available field attributes, as well as the field /// name to display better diagnostics. @@ -9,13 +9,13 @@ use crate::attrs::common::{ItemAttributes, ItemType}; /// Data stored in this struct is validated using darling's `and_then` attribute. /// During darlings validation, it is not possible to validate that action /// versions match up with declared versions on the container. This validation -/// can be done using the associated [`ValidateVersions::validate_versions`][1] +/// can be done using the associated [`FieldAttributes::validate_versions`][1] /// function. /// /// Rules shared across fields and variants can be found [here][2]. /// -/// [1]: crate::attrs::common::ValidateVersions::validate_versions -/// [2]: crate::attrs::common::ItemAttributes +/// [1]: crate::attrs::item::FieldAttributes::validate_versions +/// [2]: crate::attrs::item::CommonItemAttributes #[derive(Debug, FromField)] #[darling( attributes(versioned), @@ -24,7 +24,7 @@ use crate::attrs::common::{ItemAttributes, ItemType}; )] pub(crate) struct FieldAttributes { #[darling(flatten)] - pub(crate) common: ItemAttributes, + pub(crate) common: CommonItemAttributes, // The ident (automatically extracted by darling) cannot be moved into the // shared item attributes because for struct fields, the type is @@ -45,13 +45,18 @@ impl FieldAttributes { /// place by darling. /// /// Internally, it calls out to other specialized validation functions. - fn validate(self) -> Result { + fn validate(self) -> Result { let ident = self .ident .as_ref() - .expect("internal error: field must have an ident"); + .expect("internal error: field must have an ident") + .clone(); - self.common.validate(ident, &ItemType::Field, &self.attrs)?; + self.common.validate(FieldIdent::from(ident), &self.attrs)?; Ok(self) } + + pub(crate) fn validate_versions(&self, versions: &[VersionDefinition]) -> Result<()> { + self.common.validate_versions(versions) + } } diff --git a/crates/stackable-versioned-macros/src/attrs/item/mod.rs b/crates/stackable-versioned-macros/src/attrs/item/mod.rs new file mode 100644 index 000000000..712be5043 --- /dev/null +++ b/crates/stackable-versioned-macros/src/attrs/item/mod.rs @@ -0,0 +1,451 @@ +use std::{collections::BTreeMap, ops::Deref}; + +use darling::{util::SpannedValue, Error, FromMeta, Result}; +use k8s_version::Version; +use proc_macro2::Span; +use quote::format_ident; +use syn::{spanned::Spanned, Attribute, Path, Type}; + +use crate::{ + codegen::{ItemStatus, VersionDefinition}, + utils::ItemIdentExt, +}; + +mod field; +pub(crate) use field::*; + +mod variant; +pub(crate) use variant::*; + +/// These attributes are meant to be used in super structs, which add +/// [`Field`](syn::Field) or [`Variant`](syn::Variant) specific attributes via +/// darling's flatten feature. This struct only provides shared attributes. +/// +/// ### Shared Item Rules +/// +/// - An item can only ever be added once at most. An item not marked as 'added' +/// is part of the container in every version until changed or deprecated. +/// - An item can be changed many times. That's why changes are stored in a +/// [`Vec`]. +/// - An item can only be deprecated once. A field or variant not marked as +/// 'deprecated' will be included up until the latest version. +#[derive(Debug, FromMeta)] +pub(crate) struct CommonItemAttributes { + /// This parses the `added` attribute on items (fields or variants). It can + /// only be present at most once. + pub(crate) added: Option, + + /// This parses the `changed` attribute on items (fields or variants). It + /// can be present 0..n times. + #[darling(multiple, rename = "changed")] + pub(crate) changes: Vec, + + /// This parses the `deprecated` attribute on items (fields or variants). It + /// can only be present at most once. + pub(crate) deprecated: Option, +} + +// This impl block ONLY contains validation. The main entrypoint is the associated 'validate' +// function. In addition to validate functions which are called directly during darling's parsing, +// it contains functions which can only be called after the initial parsing and validation because +// they need additional context, namely the list of versions defined on the container or module. +impl CommonItemAttributes { + pub(crate) fn validate( + &self, + item_ident: impl ItemIdentExt, + item_attrs: &[Attribute], + ) -> Result<()> { + let mut errors = Error::accumulator(); + + errors.handle(self.validate_action_combinations(&item_ident)); + errors.handle(self.validate_action_order(&item_ident)); + errors.handle(self.validate_item_name(&item_ident)); + errors.handle(self.validate_added_action()); + errors.handle(self.validate_changed_action(&item_ident)); + errors.handle(self.validate_item_attributes(item_attrs)); + + errors.finish() + } + + pub(crate) fn validate_versions(&self, versions: &[VersionDefinition]) -> Result<()> { + let mut errors = Error::accumulator(); + + if let Some(added) = &self.added { + if !versions.iter().any(|v| v.inner == *added.since) { + errors.push(Error::custom( + "the `added` action uses a version which is not declared via `#[versioned(version)]`", + ).with_span(&added.since.span())); + } + } + + for change in &self.changes { + if !versions.iter().any(|v| v.inner == *change.since) { + errors.push(Error::custom( + "the `changed` action uses a version which is not declared via `#[versioned(version)]`" + ).with_span(&change.since.span())); + } + } + + if let Some(deprecated) = &self.deprecated { + if !versions.iter().any(|v| v.inner == *deprecated.since) { + errors.push(Error::custom( + "the `deprecated` action uses a version which is not declared via `#[versioned(version)]`", + ).with_span(&deprecated.since.span())); + } + } + + errors.finish() + } + + /// This associated function is called by the top-level validation function + /// and validates that each item uses a valid combination of actions. + /// Invalid combinations are: + /// + /// - `added` and `deprecated` using the same version: A field or variant + /// cannot be marked as added in a particular version and then marked as + /// deprecated immediately after. Fields and variants must be included for + /// at least one version before being marked deprecated. + /// - `added` and `changed` using the same version: The same reasoning from + /// above applies here as well. Fields and variants must be included for + /// at least one version before being changed. + /// - `changed` and `deprecated` using the same version: Again, the same + /// rules from above apply here as well. + fn validate_action_combinations(&self, item_ident: &impl ItemIdentExt) -> Result<()> { + match (&self.added, &self.changes, &self.deprecated) { + (Some(added), _, Some(deprecated)) if *added.since == *deprecated.since => Err( + Error::custom("cannot be marked as `added` and `deprecated` in the same version") + .with_span(item_ident), + ), + (Some(added), changed, _) if changed.iter().any(|r| *r.since == *added.since) => Err( + Error::custom("cannot be marked as `added` and `changed` in the same version") + .with_span(item_ident), + ), + (_, changed, Some(deprecated)) + if changed.iter().any(|r| *r.since == *deprecated.since) => + { + Err(Error::custom( + "cannot be marked as `deprecated` and `changed` in the same version", + ) + .with_span(item_ident)) + } + _ => Ok(()), + } + } + + /// This associated function is called by the top-level validation function + /// and validates that actions use a chronologically sound chain of + /// versions. + /// + /// The following rules apply: + /// + /// - `deprecated` must use a greater version than `added`: This function + /// ensures that these versions are chronologically sound, that means, + /// that the version of the deprecated action must be greater than the + /// version of the added action. + /// - All `changed` actions must use a greater version than `added` but a + /// lesser version than `deprecated`. + fn validate_action_order(&self, item_ident: &impl ItemIdentExt) -> Result<()> { + let added_version = self.added.as_ref().map(|a| *a.since); + let deprecated_version = self.deprecated.as_ref().map(|d| *d.since); + + // 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 { + return Err(Error::custom(format!( + "cannot marked as `added` in version `{added_version}` while being marked as `deprecated` in an earlier version `{deprecated_version}`" + )).with_span(item_ident)); + } + } + + // Now, iterate over all changes and ensure that their versions are + // between the added and deprecated version. + if !self.changes.iter().all(|r| { + added_version.map_or(true, |a| a < *r.since) + && deprecated_version.map_or(true, |d| d > *r.since) + }) { + return Err(Error::custom( + "all changes must use versions higher than `added` and lower than `deprecated`", + ) + .with_span(item_ident)); + } + + Ok(()) + } + + /// This associated function is called by the top-level validation function + /// and validates that items use correct names depending on attached + /// actions. + /// + /// The following naming rules apply: + /// + /// - Fields or variants marked as deprecated need to include the + /// deprecation prefix in their name. The prefix must not be included for + /// fields or variants which are not deprecated. + fn validate_item_name(&self, item_ident: &impl ItemIdentExt) -> Result<()> { + let starts_with_deprecated = item_ident.starts_with_deprecated_prefix(); + + if self.deprecated.is_some() && !starts_with_deprecated { + return Err(Error::custom(format!( + "marked as `deprecated` and thus must include the `{deprecated_prefix}` prefix", + deprecated_prefix = item_ident.deprecated_prefix() + )) + .with_span(item_ident)); + } + + if self.deprecated.is_none() && starts_with_deprecated { + return Err(Error::custom( + format!("not marked as `deprecated` and thus must not include the `{deprecated_prefix}` prefix", deprecated_prefix = item_ident.deprecated_prefix()) + ).with_span(item_ident)); + } + + Ok(()) + } + + /// This associated function is called by the top-level validation function + /// and validates that parameters provided to the `added` actions are + /// valid. + fn validate_added_action(&self) -> Result<()> { + // NOTE (@Techassi): Can the path actually be empty? + if let Some(added) = &self.added { + if added.default_fn.segments.is_empty() { + return Err(Error::custom("`default_fn` cannot be empty") + .with_span(&added.default_fn.span())); + } + } + + Ok(()) + } + + /// This associated function is called by the top-level validation function + /// and validates that parameters provided to the `changed` actions are + /// valid. + fn validate_changed_action(&self, item_ident: &impl ItemIdentExt) -> Result<()> { + let mut errors = Error::accumulator(); + + // This ensures that `from_name` doesn't include the deprecation prefix. + for change in &self.changes { + if let Some(from_name) = change.from_name.as_ref() { + if from_name.starts_with(item_ident.deprecated_prefix()) { + errors.push( + Error::custom( + "the previous name must not start with the deprecation prefix", + ) + .with_span(&from_name.span()), + ); + } + } + } + + errors.finish() + } + + /// This associated function is called by the top-level validation function + /// and validates that disallowed item attributes are not used. + /// + /// The following naming rules apply: + /// + /// - `deprecated` must not be set on items. Instead, use the `deprecated()` + /// action of the `#[versioned()]` macro. + fn validate_item_attributes(&self, item_attrs: &[Attribute]) -> Result<()> { + for attr in item_attrs { + for segment in &attr.path().segments { + if segment.ident == "deprecated" { + return Err(Error::custom("deprecation must be done using `#[versioned(deprecated(since = \"VERSION\"))]`") + .with_span(&attr.span())); + } + } + } + Ok(()) + } +} + +impl CommonItemAttributes { + pub(crate) fn into_changeset( + self, + ident: &impl ItemIdentExt, + ty: Type, + ) -> Option> { + // TODO (@Techassi): Use Change instead of ItemStatus + if let Some(deprecated) = self.deprecated { + let deprecated_ident = ident.deref(); + + // When the item is deprecated, any change which occurred beforehand + // requires access to the item ident to infer the item ident for + // the latest change. + let mut ident = ident.as_cleaned_ident(); + let mut ty = ty; + + let mut actions = BTreeMap::new(); + + actions.insert( + *deprecated.since, + ItemStatus::Deprecation { + previous_ident: ident.clone(), + ident: deprecated_ident.clone(), + note: deprecated.note.as_deref().cloned(), + }, + ); + + for change in self.changes.iter().rev() { + let from_ident = if let Some(from) = change.from_name.as_deref() { + format_ident!("{from}").into() + } else { + ident.clone() + }; + + // TODO (@Techassi): This is an awful lot of cloning, can we get + // rid of it? + let from_ty = change + .from_type + .as_ref() + .map(|sv| sv.deref().clone()) + .unwrap_or(ty.clone()); + + actions.insert( + *change.since, + ItemStatus::Change { + from_ident: from_ident.clone(), + to_ident: ident, + from_type: from_ty.clone(), + to_type: ty, + }, + ); + + ident = from_ident; + ty = from_ty; + } + + // After the last iteration above (if any) we use the ident for the + // added action if there is any. + if let Some(added) = self.added { + actions.insert( + *added.since, + ItemStatus::Addition { + default_fn: added.default_fn.deref().clone(), + ident, + ty, + }, + ); + } + + Some(actions) + } else if !self.changes.is_empty() { + let mut ident = ident.deref().clone(); + let mut ty = ty; + + let mut actions = BTreeMap::new(); + + for change in self.changes.iter().rev() { + let from_ident = if let Some(from) = change.from_name.as_deref() { + format_ident!("{from}").into() + } else { + ident.clone() + }; + + // TODO (@Techassi): This is an awful lot of cloning, can we get + // rid of it? + let from_ty = change + .from_type + .as_ref() + .map(|sv| sv.deref().clone()) + .unwrap_or(ty.clone()); + + actions.insert( + *change.since, + ItemStatus::Change { + from_ident: from_ident.clone(), + to_ident: ident, + from_type: from_ty.clone(), + to_type: ty, + }, + ); + + ident = from_ident; + ty = from_ty; + } + + // After the last iteration above (if any) we use the ident for the + // added action if there is any. + if let Some(added) = self.added { + actions.insert( + *added.since, + ItemStatus::Addition { + default_fn: added.default_fn.deref().clone(), + ident, + ty, + }, + ); + } + + Some(actions) + } else { + if let Some(added) = self.added { + let mut actions = BTreeMap::new(); + + actions.insert( + *added.since, + ItemStatus::Addition { + default_fn: added.default_fn.deref().clone(), + ident: ident.deref().clone(), + ty, + }, + ); + + return Some(actions); + } + + None + } + } +} + +/// For the added() action +/// +/// Example usage: +/// - `added(since = "...")` +/// - `added(since = "...", default_fn = "custom_fn")` +#[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") + .expect("internal error: path must parse"), + Span::call_site(), + ) +} + +// TODO (@Techassi): Add validation for when from_name AND from_type are both +// none => is this action needed in the first place? +// TODO (@Techassi): Add validation that the from_name mustn't include the +// deprecated prefix. +/// For the changed() action +/// +/// Example usage: +/// - `changed(since = "...", from_name = "...")` +/// - `changed(since = "...", from_name = "..." from_type="...")` +#[derive(Clone, Debug, FromMeta)] +pub(crate) struct ChangedAttributes { + pub(crate) since: SpannedValue, + pub(crate) from_name: Option>, + pub(crate) from_type: Option>, +} + +/// For the deprecated() action +/// +/// Example usage: +/// - `deprecated(since = "...")` +/// - `deprecated(since = "...", note = "...")` +#[derive(Clone, Debug, FromMeta)] +pub(crate) struct DeprecatedAttributes { + pub(crate) since: SpannedValue, + pub(crate) note: Option>, +} diff --git a/crates/stackable-versioned-macros/src/attrs/variant.rs b/crates/stackable-versioned-macros/src/attrs/item/variant.rs similarity index 75% rename from crates/stackable-versioned-macros/src/attrs/variant.rs rename to crates/stackable-versioned-macros/src/attrs/item/variant.rs index bcdde256a..42dc58149 100644 --- a/crates/stackable-versioned-macros/src/attrs/variant.rs +++ b/crates/stackable-versioned-macros/src/attrs/item/variant.rs @@ -1,8 +1,8 @@ use convert_case::{Case, Casing}; -use darling::{Error, FromVariant}; +use darling::{Error, FromVariant, Result}; use syn::{Attribute, Ident}; -use crate::attrs::common::{ItemAttributes, ItemType}; +use crate::{attrs::item::CommonItemAttributes, codegen::VersionDefinition, utils::VariantIdent}; /// This struct describes all available variant attributes, as well as the /// variant name to display better diagnostics. @@ -10,13 +10,13 @@ use crate::attrs::common::{ItemAttributes, ItemType}; /// Data stored in this struct is validated using darling's `and_then` attribute. /// During darlings validation, it is not possible to validate that action /// versions match up with declared versions on the container. This validation -/// can be done using the associated [`ValidateVersions::validate_versions`][1] +/// can be done using the associated [`VariantAttributes::validate_versions`][1] /// function. /// /// Rules shared across fields and variants can be found [here][2]. /// -/// [1]: crate::attrs::common::ValidateVersions::validate_versions -/// [2]: crate::attrs::common::ItemAttributes +/// [1]: crate::attrs::item::VariantAttributes::validate_versions +/// [2]: crate::attrs::item::CommonItemAttributes #[derive(Debug, FromVariant)] #[darling( attributes(versioned), @@ -25,7 +25,7 @@ use crate::attrs::common::{ItemAttributes, ItemType}; )] pub(crate) struct VariantAttributes { #[darling(flatten)] - pub(crate) common: ItemAttributes, + pub(crate) common: CommonItemAttributes, // The ident (automatically extracted by darling) cannot be moved into the // shared item attributes because for struct fields, the type is @@ -46,12 +46,12 @@ impl VariantAttributes { /// place by darling. /// /// Internally, it calls out to other specialized validation functions. - fn validate(self) -> Result { + fn validate(self) -> Result { let mut errors = Error::accumulator(); errors.handle( self.common - .validate(&self.ident, &ItemType::Variant, &self.attrs), + .validate(VariantIdent::from(self.ident.clone()), &self.attrs), ); // Validate names of renames @@ -66,7 +66,10 @@ impl VariantAttributes { } } - errors.finish()?; - Ok(self) + errors.finish_with(self) + } + + pub(crate) fn validate_versions(&self, versions: &[VersionDefinition]) -> Result<()> { + self.common.validate_versions(versions) } } diff --git a/crates/stackable-versioned-macros/src/attrs/k8s.rs b/crates/stackable-versioned-macros/src/attrs/k8s.rs new file mode 100644 index 000000000..085136925 --- /dev/null +++ b/crates/stackable-versioned-macros/src/attrs/k8s.rs @@ -0,0 +1,38 @@ +use darling::{util::Flag, FromMeta}; + +/// This struct contains supported Kubernetes arguments. +/// +/// The arguments are passed through to the `#[kube]` attribute. More details can be found in the +/// official docs: . +/// +/// Supported arguments are: +/// +/// - `skip`, which controls skipping parts of the generation. +/// - `singular`, to specify the singular name of the CR object. +/// - `plural`, to specify the plural name of the CR object. +/// - `kind`, which allows overwriting the kind field of the CRD. This defaults to the struct name +/// (without the 'Spec' suffix). +/// - `namespaced`, to specify that this is a namespaced resource rather than cluster level. +/// - `group`, which sets the CRD group, usually the domain of the company. +#[derive(Clone, Debug, FromMeta)] +pub(crate) struct KubernetesArguments { + pub(crate) skip: Option, + pub(crate) singular: Option, + pub(crate) plural: Option, + pub(crate) kind: Option, + pub(crate) namespaced: Flag, + pub(crate) group: String, +} + +/// This struct contains supported kubernetes skip arguments. +/// +/// Supported arguments are: +/// +/// - `merged_crd` flag, which skips generating the `crd()` and `merged_crd()` functions are +/// generated. +#[derive(Clone, Debug, FromMeta)] +pub(crate) struct KubernetesSkipArguments { + /// Whether the `crd()` and `merged_crd()` generation should be skipped for + /// this container. + pub(crate) merged_crd: Flag, +} diff --git a/crates/stackable-versioned-macros/src/attrs/mod.rs b/crates/stackable-versioned-macros/src/attrs/mod.rs index 6eb6b96bd..e7c06ef8d 100644 --- a/crates/stackable-versioned-macros/src/attrs/mod.rs +++ b/crates/stackable-versioned-macros/src/attrs/mod.rs @@ -1,3 +1,5 @@ pub(crate) mod common; -pub(crate) mod field; -pub(crate) mod variant; +pub(crate) mod container; +pub(crate) mod item; +pub(crate) mod k8s; +pub(crate) mod module; diff --git a/crates/stackable-versioned-macros/src/attrs/module.rs b/crates/stackable-versioned-macros/src/attrs/module.rs new file mode 100644 index 000000000..b6c3b5ee4 --- /dev/null +++ b/crates/stackable-versioned-macros/src/attrs/module.rs @@ -0,0 +1,10 @@ +use darling::{util::Flag, FromMeta}; + +use crate::attrs::common::CommonRootArguments; + +#[derive(Debug, FromMeta)] +pub(crate) struct ModuleAttributes { + #[darling(flatten)] + pub(crate) common_root_arguments: CommonRootArguments, + pub(crate) preserve_module: Flag, +} diff --git a/crates/stackable-versioned-macros/src/codegen/chain.rs b/crates/stackable-versioned-macros/src/codegen/chain.rs deleted file mode 100644 index 2376e3ffd..000000000 --- a/crates/stackable-versioned-macros/src/codegen/chain.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::{collections::BTreeMap, ops::Bound}; - -pub(crate) trait Neighbors -where - K: Ord + Eq, -{ - /// Returns the values of keys which are neighbors of `key`. - /// - /// Given a map which contains the following keys: 1, 3, 5. Calling this - /// function with these keys, results in the following return values: - /// - /// - Key **0**: `(None, Some(1))` - /// - Key **2**: `(Some(1), Some(3))` - /// - Key **4**: `(Some(3), Some(5))` - /// - Key **6**: `(Some(5), None)` - fn get_neighbors(&self, key: &K) -> (Option<&V>, Option<&V>); - - /// Returns whether the function `f` returns true if applied to the value - /// identified by `key`. - fn value_is(&self, key: &K, f: F) -> bool - where - F: Fn(&V) -> bool; - - fn lo_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)>; - fn up_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)>; -} - -impl Neighbors for BTreeMap -where - K: Ord + Eq, -{ - fn get_neighbors(&self, key: &K) -> (Option<&V>, Option<&V>) { - // NOTE (@Techassi): These functions might get added to the standard - // library at some point. If that's the case, we can use the ones - // provided by the standard lib. - // See: https://github.com/rust-lang/rust/issues/107540 - match ( - self.lo_bound(Bound::Excluded(key)), - self.up_bound(Bound::Excluded(key)), - ) { - (Some((k, v)), None) => { - if key > k { - (Some(v), None) - } else { - (self.lo_bound(Bound::Excluded(k)).map(|(_, v)| v), None) - } - } - (None, Some((k, v))) => { - if key < k { - (None, Some(v)) - } else { - (None, self.up_bound(Bound::Excluded(k)).map(|(_, v)| v)) - } - } - (Some((_, lo)), Some((_, up))) => (Some(lo), Some(up)), - (None, None) => unreachable!(), - } - } - - fn value_is(&self, key: &K, f: F) -> bool - where - F: Fn(&V) -> bool, - { - self.get(key).map_or(false, f) - } - - fn lo_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)> { - self.range((Bound::Unbounded, bound)).next_back() - } - - fn up_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)> { - self.range((bound, Bound::Unbounded)).next() - } -} - -pub(crate) trait BTreeMapExt -where - K: Ord, -{ - const MESSAGE: &'static str; - - fn get_expect(&self, key: &K) -> &V; -} - -impl BTreeMapExt for BTreeMap -where - K: Ord, -{ - const MESSAGE: &'static str = "internal error: chain must contain version"; - - fn get_expect(&self, key: &K) -> &V { - self.get(key).expect(Self::MESSAGE) - } -} - -#[cfg(test)] -mod test { - use super::*; - use rstest::rstest; - - #[rstest] - #[case(0, (None, Some(&"test1")))] - #[case(1, (None, Some(&"test3")))] - #[case(2, (Some(&"test1"), Some(&"test3")))] - #[case(3, (Some(&"test1"), None))] - #[case(4, (Some(&"test3"), None))] - fn neighbors(#[case] key: i32, #[case] expected: (Option<&&str>, Option<&&str>)) { - let map = BTreeMap::from([(1, "test1"), (3, "test3")]); - let neigbors = map.get_neighbors(&key); - - assert_eq!(neigbors, expected); - } -} diff --git a/crates/stackable-versioned-macros/src/codegen/changes.rs b/crates/stackable-versioned-macros/src/codegen/changes.rs new file mode 100644 index 000000000..df52a38ca --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/changes.rs @@ -0,0 +1,220 @@ +use std::{collections::BTreeMap, ops::Bound}; + +use k8s_version::Version; +use syn::Type; + +use crate::codegen::{ItemStatus, VersionDefinition}; + +pub(crate) trait Neighbors +where + K: Ord + Eq, +{ + /// Returns the values of keys which are neighbors of `key`. + /// + /// Given a map which contains the following keys: 1, 3, 5. Calling this + /// function with these keys, results in the following return values: + /// + /// - Key **0**: `(None, Some(1))` + /// - Key **2**: `(Some(1), Some(3))` + /// - Key **4**: `(Some(3), Some(5))` + /// - Key **6**: `(Some(5), None)` + fn get_neighbors(&self, key: &K) -> (Option<&V>, Option<&V>); + + /// Returns whether the function `f` returns true if applied to the value + /// identified by `key`. + fn value_is(&self, key: &K, f: F) -> bool + where + F: Fn(&V) -> bool; + + fn lo_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)>; + fn up_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)>; +} + +impl Neighbors for BTreeMap +where + K: Ord + Eq, +{ + fn get_neighbors(&self, key: &K) -> (Option<&V>, Option<&V>) { + // NOTE (@Techassi): These functions might get added to the standard + // library at some point. If that's the case, we can use the ones + // provided by the standard lib. + // See: https://github.com/rust-lang/rust/issues/107540 + match ( + self.lo_bound(Bound::Excluded(key)), + self.up_bound(Bound::Excluded(key)), + ) { + (Some((k, v)), None) => { + if key > k { + (Some(v), None) + } else { + (self.lo_bound(Bound::Excluded(k)).map(|(_, v)| v), None) + } + } + (None, Some((k, v))) => { + if key < k { + (None, Some(v)) + } else { + (None, self.up_bound(Bound::Excluded(k)).map(|(_, v)| v)) + } + } + (Some((_, lo)), Some((_, up))) => (Some(lo), Some(up)), + (None, None) => unreachable!(), + } + } + + fn value_is(&self, key: &K, f: F) -> bool + where + F: Fn(&V) -> bool, + { + self.get(key).map_or(false, f) + } + + fn lo_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)> { + self.range((Bound::Unbounded, bound)).next_back() + } + + fn up_bound(&self, bound: Bound<&K>) -> Option<(&K, &V)> { + self.range((bound, Bound::Unbounded)).next() + } +} + +pub(crate) trait BTreeMapExt +where + K: Ord, +{ + const MESSAGE: &'static str; + + fn get_expect(&self, key: &K) -> &V; +} + +impl BTreeMapExt for BTreeMap { + const MESSAGE: &'static str = "internal error: chain must contain version"; + + fn get_expect(&self, key: &Version) -> &V { + self.get(key).expect(Self::MESSAGE) + } +} + +pub(crate) trait ChangesetExt { + fn insert_container_versions(&mut self, versions: &[VersionDefinition], ty: &Type); +} + +impl ChangesetExt for BTreeMap { + fn insert_container_versions(&mut self, versions: &[VersionDefinition], ty: &Type) { + for version in versions { + if self.contains_key(&version.inner) { + continue; + } + + match self.get_neighbors(&version.inner) { + (None, Some(status)) => match status { + ItemStatus::Addition { .. } => { + self.insert(version.inner, ItemStatus::NotPresent) + } + ItemStatus::Change { + from_ident, + from_type, + .. + } => self.insert( + version.inner, + ItemStatus::NoChange { + previously_deprecated: false, + ident: from_ident.clone(), + ty: from_type.clone(), + }, + ), + ItemStatus::Deprecation { previous_ident, .. } => self.insert( + version.inner, + ItemStatus::NoChange { + previously_deprecated: false, + ident: previous_ident.clone(), + ty: ty.clone(), + }, + ), + ItemStatus::NoChange { + previously_deprecated, + ident, + ty, + } => self.insert( + version.inner, + ItemStatus::NoChange { + previously_deprecated: *previously_deprecated, + ident: ident.clone(), + ty: ty.clone(), + }, + ), + ItemStatus::NotPresent => unreachable!(), + }, + (Some(status), None) => { + let (ident, ty, previously_deprecated) = match status { + ItemStatus::Addition { ident, ty, .. } => (ident, ty, false), + ItemStatus::Change { + to_ident, to_type, .. + } => (to_ident, to_type, false), + ItemStatus::Deprecation { ident, .. } => (ident, ty, true), + ItemStatus::NoChange { + previously_deprecated, + ident, + ty, + .. + } => (ident, ty, *previously_deprecated), + ItemStatus::NotPresent => unreachable!(), + }; + + self.insert( + version.inner, + ItemStatus::NoChange { + previously_deprecated, + ident: ident.clone(), + ty: ty.clone(), + }, + ) + } + (Some(status), Some(_)) => { + let (ident, ty, previously_deprecated) = match status { + ItemStatus::Addition { ident, ty, .. } => (ident, ty, false), + ItemStatus::Change { + to_ident, to_type, .. + } => (to_ident, to_type, false), + ItemStatus::NoChange { + previously_deprecated, + ident, + ty, + .. + } => (ident, ty, *previously_deprecated), + _ => unreachable!(), + }; + + self.insert( + version.inner, + ItemStatus::NoChange { + previously_deprecated, + ident: ident.clone(), + ty: ty.clone(), + }, + ) + } + _ => unreachable!(), + }; + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use rstest::rstest; + + #[rstest] + #[case(0, (None, Some(&"test1")))] + #[case(1, (None, Some(&"test3")))] + #[case(2, (Some(&"test1"), Some(&"test3")))] + #[case(3, (Some(&"test1"), None))] + #[case(4, (Some(&"test3"), None))] + fn neighbors(#[case] key: i32, #[case] expected: (Option<&&str>, Option<&&str>)) { + let map = BTreeMap::from([(1, "test1"), (3, "test3")]); + let neigbors = map.get_neighbors(&key); + + assert_eq!(neigbors, expected); + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/common/container.rs b/crates/stackable-versioned-macros/src/codegen/common/container.rs deleted file mode 100644 index 4472552d9..000000000 --- a/crates/stackable-versioned-macros/src/codegen/common/container.rs +++ /dev/null @@ -1,190 +0,0 @@ -use std::ops::Deref; - -use convert_case::{Case, Casing}; -use k8s_version::Version; -use proc_macro2::TokenStream; -use quote::format_ident; -use syn::{Attribute, Ident, Visibility}; - -use crate::{attrs::common::ContainerAttributes, codegen::common::ContainerVersion}; - -/// This trait helps to unify versioned containers, like structs and enums. -/// -/// This trait is implemented by wrapper structs, which wrap the generic -/// [`VersionedContainer`] struct. The generic type parameter `D` describes the -/// kind of data, like [`DataStruct`](syn::DataStruct) in case of a struct and -/// [`DataEnum`](syn::DataEnum) in case of an enum. -/// The type parameter `I` describes the type of the versioned items, like -/// [`VersionedField`][1] and [`VersionedVariant`][2]. -/// -/// [1]: crate::codegen::vstruct::field::VersionedField -/// [2]: crate::codegen::venum::variant::VersionedVariant -pub(crate) trait Container -where - Self: Sized + Deref>, -{ - /// Creates a new versioned container. - fn new(input: ContainerInput, data: D, attributes: ContainerAttributes) -> syn::Result; - - /// This generates the complete code for a single versioned container. - /// - /// Internally, it will create a module for each declared version which - /// contains the container with the appropriate items (fields or variants) - /// Additionally, it generates `From` implementations, which enable - /// conversion from an older to a newer version. - fn generate_tokens(&self) -> TokenStream; -} - -/// Provides extra functionality on top of [`struct@Ident`]s. -pub(crate) trait IdentExt { - /// Removes the 'Spec' suffix from the [`struct@Ident`]. - fn as_cleaned_kubernetes_ident(&self) -> Ident; - - /// Transforms the [`struct@Ident`] into one usable in the [`From`] impl. - fn as_from_impl_ident(&self) -> Ident; -} - -impl IdentExt for Ident { - fn as_cleaned_kubernetes_ident(&self) -> Ident { - format_ident!("{}", self.to_string().trim_end_matches("Spec")) - } - - fn as_from_impl_ident(&self) -> Ident { - format_ident!("__sv_{}", self.to_string().to_lowercase()) - } -} - -pub(crate) trait VersionExt { - fn as_variant_ident(&self) -> Ident; -} - -impl VersionExt for Version { - fn as_variant_ident(&self) -> Ident { - format_ident!("{ident}", ident = self.to_string().to_case(Case::Pascal)) - } -} - -/// This struct bundles values from [`DeriveInput`][1]. -/// -/// [`DeriveInput`][1] cannot be used directly when constructing a -/// [`VersionedStruct`][2] or [`VersionedEnum`][3] because we run into borrow -/// issues caused by the match statement which extracts the data. -/// -/// [1]: syn::DeriveInput -/// [2]: crate::codegen::vstruct::VersionedStruct -/// [3]: crate::codegen::venum::VersionedEnum -pub(crate) struct ContainerInput { - pub(crate) original_attributes: Vec, - pub(crate) visibility: Visibility, - pub(crate) ident: Ident, -} - -/// Stores individual versions of a single container. -/// -/// Each version tracks item actions, which describe if the item was added, -/// renamed or deprecated in that particular version. Items which are not -/// versioned are included in every version of the container. -#[derive(Debug)] -pub(crate) struct VersionedContainer { - /// List of declared versions for this container. Each version generates a - /// definition with appropriate items. - pub(crate) versions: Vec, - - /// The original attributes that were added to the container. - pub(crate) original_attributes: Vec, - - /// The visibility of the versioned container. Used to forward the - /// visibility during code generation. - pub(crate) visibility: Visibility, - - /// List of items defined in the original container. How, and if, an item - /// should generate code, is decided by the currently generated version. - pub(crate) items: Vec, - - /// Different options which influence code generation. - pub(crate) options: VersionedContainerOptions, - - /// A collection of container idents used for different purposes. - pub(crate) idents: VersionedContainerIdents, -} - -impl VersionedContainer { - /// Creates a new versioned Container which contains common data shared - /// across structs and enums. - pub(crate) fn new( - input: ContainerInput, - attributes: ContainerAttributes, - versions: Vec, - items: Vec, - ) -> Self { - let ContainerInput { - original_attributes, - visibility, - ident, - } = input; - - let skip_from = attributes - .common_option_attrs - .skip - .map_or(false, |s| s.from.is_present()); - - let kubernetes_options = attributes.kubernetes_attrs.map(|a| KubernetesOptions { - skip_merged_crd: a.skip.map_or(false, |s| s.merged_crd.is_present()), - namespaced: a.namespaced.is_present(), - singular: a.singular, - plural: a.plural, - group: a.group, - kind: a.kind, - }); - - let options = VersionedContainerOptions { - kubernetes_options, - skip_from, - }; - - let idents = VersionedContainerIdents { - kubernetes: ident.as_cleaned_kubernetes_ident(), - from: ident.as_from_impl_ident(), - original: ident, - }; - - VersionedContainer { - original_attributes, - visibility, - versions, - options, - idents, - items, - } - } -} - -/// A collection of container idents used for different purposes. -#[derive(Debug)] -pub(crate) struct VersionedContainerIdents { - /// The ident used in the context of Kubernetes specific code. This ident - /// removes the 'Spec' suffix present in the definition container. - pub(crate) kubernetes: Ident, - - /// The original ident, or name, of the versioned container. - pub(crate) original: Ident, - - /// The ident used in the [`From`] impl. - pub(crate) from: Ident, -} - -#[derive(Debug)] -pub(crate) struct VersionedContainerOptions { - pub(crate) kubernetes_options: Option, - pub(crate) skip_from: bool, -} - -#[derive(Debug)] -pub(crate) struct KubernetesOptions { - pub(crate) singular: Option, - pub(crate) plural: Option, - pub(crate) skip_merged_crd: bool, - pub(crate) kind: Option, - pub(crate) namespaced: bool, - pub(crate) group: String, -} diff --git a/crates/stackable-versioned-macros/src/codegen/common/item.rs b/crates/stackable-versioned-macros/src/codegen/common/item.rs deleted file mode 100644 index d040e637f..000000000 --- a/crates/stackable-versioned-macros/src/codegen/common/item.rs +++ /dev/null @@ -1,439 +0,0 @@ -use std::{collections::BTreeMap, marker::PhantomData, ops::Deref}; - -use quote::format_ident; -use syn::{spanned::Spanned, Attribute, Ident, Path, Type}; - -use crate::{ - attrs::common::{ContainerAttributes, ItemAttributes, ValidateVersions}, - codegen::{ - chain::Neighbors, - common::{ContainerVersion, VersionChain}, - }, -}; - -/// This trait describes versioned container items, fields and variants in a -/// common way. -/// -/// Shared functionality is implemented in a single place. Code which cannot be -/// shared is implemented on the wrapping type, like [`VersionedField`][1]. -/// -/// [1]: crate::codegen::vstruct::field::VersionedField -pub(crate) trait Item: Sized -where - A: for<'i> TryFrom<&'i I> + Attributes, - I: InnerItem, -{ - /// Creates a new versioned item (struct field or enum variant) by consuming - /// the parsed [Field](syn::Field) or [Variant](syn::Variant) and validating - /// the versions of field actions against versions attached on the container. - fn new(item: I, container_attrs: &ContainerAttributes) -> syn::Result; - - /// Inserts container versions not yet present in the status chain. - /// - /// When initially creating a new versioned item, 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 continuous chain ensures that when generating code (tokens), each - /// field can lookup the status (and ident) for a requested version. - fn insert_container_versions(&mut self, versions: &[ContainerVersion]); - - /// Returns the ident of the item based on the provided container version. - fn get_ident(&self, version: &ContainerVersion) -> Option<&Ident>; -} - -pub(crate) trait InnerItem: Named + Spanned { - fn ty(&self) -> Type; -} - -/// This trait enables access to the ident of named items, like fields and -/// variants. -/// -/// It additionally provides a function to retrieve the cleaned ident, which -/// removes the deprecation prefixes. -pub(crate) trait Named { - fn cleaned_ident(&self) -> Ident; - fn ident(&self) -> &Ident; -} - -/// This trait enables access to the common and original attributes across field -/// and variant attributes. -pub(crate) trait Attributes { - /// The common attributes defined by the versioned macro. - fn common_attributes_owned(self) -> ItemAttributes; - - /// The common attributes defined by the versioned macro. - fn common_attributes(&self) -> &ItemAttributes; - - /// The attributes applied to the item outside of the versioned macro. - fn original_attributes(&self) -> &Vec; -} - -/// This struct combines common code for versioned fields and variants. -/// -/// Most of the initial creation of a versioned field and variant are identical. -/// Currently, the following steps are unified: -/// -/// - Initial creation of the action chain based on item attributes. -/// - Insertion of container versions into the chain. -/// -/// The generic type parameter `I` describes the type of the versioned item, -/// usually [`Field`](syn::Field) or [`Variant`](syn::Variant). The parameter -/// `A` indicates the type of item attributes, usually [`FieldAttributes`][1] or -/// [`VariantAttributes`][2] depending on the used item type. As this type is -/// only needed during creation of [`Self`](VersionedItem), we must use a -/// [`PhantomData`] marker. -/// -/// [1]: crate::attrs::field::FieldAttributes -/// [2]: crate::attrs::variant::VariantAttributes -#[derive(Debug)] -pub(crate) struct VersionedItem -where - A: for<'i> TryFrom<&'i I> + Attributes, - I: InnerItem, -{ - pub(crate) original_attributes: Vec, - pub(crate) chain: Option, - pub(crate) inner: I, - _marker: PhantomData, -} - -impl Item for VersionedItem -where - syn::Error: for<'i> From<>::Error>, - A: for<'i> TryFrom<&'i I> + Attributes + ValidateVersions, - I: InnerItem, -{ - fn new(item: I, container_attrs: &ContainerAttributes) -> syn::Result { - // We use the TryFrom trait here, because the type parameter `A` can use - // it as a trait bound. Internally this then calls either `from_field` - // for field attributes or `from_variant` for variant attributes. Sadly - // darling doesn't provide a "generic" trait which abstracts over the - // different `from_` functions. - let attrs = A::try_from(&item)?; - attrs.validate_versions(container_attrs, &item)?; - - // These are the attributes added to the item outside of the macro. - let original_attributes = attrs.original_attributes().clone(); - - // These are the versioned macro attrs that are common to all items. - let common_attributes = attrs.common_attributes_owned(); - - // Constructing the action chain requires going through the actions - // starting at the end, because the container definition always - // represents the latest (most up-to-date) version of that struct. - // That's why the following code needs to go through the actions in - // reverse order, as otherwise it is impossible to extract the item - // ident for each version. - - // Deprecating an item is always the last state an item can end up in. - // For items which are not deprecated, the last change is either the - // latest change or addition, which is handled below. The ident of the - // deprecated item is guaranteed to include the 'deprecated_' or - // 'DEPRECATED_' prefix. The ident can thus be used as is. - if let Some(deprecated) = common_attributes.deprecated { - let deprecated_ident = item.ident(); - - // When the item is deprecated, any change which occurred beforehand - // requires access to the item ident to infer the item ident for - // the latest change. - let mut ident = item.cleaned_ident(); - let mut ty = item.ty(); - - let mut actions = BTreeMap::new(); - - actions.insert( - *deprecated.since, - ItemStatus::Deprecation { - previous_ident: ident.clone(), - ident: deprecated_ident.clone(), - note: deprecated.note.as_deref().cloned(), - }, - ); - - for change in common_attributes.changes.iter().rev() { - let from_ident = if let Some(from) = change.from_name.as_deref() { - format_ident!("{from}") - } else { - ident.clone() - }; - - // TODO (@Techassi): This is an awful lot of cloning, can we get - // rid of it? - let from_ty = change - .from_type - .as_ref() - .map(|sv| sv.deref().clone()) - .unwrap_or(ty.clone()); - - actions.insert( - *change.since, - ItemStatus::Change { - from_ident: from_ident.clone(), - to_ident: ident, - from_type: from_ty.clone(), - to_type: ty, - }, - ); - - ident = from_ident; - ty = from_ty; - } - - // After the last iteration above (if any) we use the ident for the - // added action if there is any. - if let Some(added) = common_attributes.added { - actions.insert( - *added.since, - ItemStatus::Addition { - default_fn: added.default_fn.deref().clone(), - ident, - ty, - }, - ); - } - - Ok(Self { - _marker: PhantomData, - chain: Some(actions), - original_attributes, - inner: item, - }) - } else if !common_attributes.changes.is_empty() { - let mut ident = item.ident().clone(); - let mut ty = item.ty(); - - let mut actions = BTreeMap::new(); - - for change in common_attributes.changes.iter().rev() { - let from_ident = if let Some(from) = change.from_name.as_deref() { - format_ident!("{from}") - } else { - ident.clone() - }; - - // TODO (@Techassi): This is an awful lot of cloning, can we get - // rid of it? - let from_ty = change - .from_type - .as_ref() - .map(|sv| sv.deref().clone()) - .unwrap_or(ty.clone()); - - actions.insert( - *change.since, - ItemStatus::Change { - from_ident: from_ident.clone(), - to_ident: ident, - from_type: from_ty.clone(), - to_type: ty, - }, - ); - - ident = from_ident; - ty = from_ty; - } - - // After the last iteration above (if any) we use the ident for the - // added action if there is any. - if let Some(added) = common_attributes.added { - actions.insert( - *added.since, - ItemStatus::Addition { - default_fn: added.default_fn.deref().clone(), - ident, - ty, - }, - ); - } - - Ok(Self { - _marker: PhantomData, - chain: Some(actions), - original_attributes, - inner: item, - }) - } else { - if let Some(added) = common_attributes.added { - let mut actions = BTreeMap::new(); - - actions.insert( - *added.since, - ItemStatus::Addition { - default_fn: added.default_fn.deref().clone(), - ident: item.ident().clone(), - ty: item.ty(), - }, - ); - - return Ok(Self { - _marker: PhantomData, - chain: Some(actions), - original_attributes, - inner: item, - }); - } - - Ok(Self { - _marker: PhantomData, - original_attributes, - chain: None, - inner: item, - }) - } - } - - fn insert_container_versions(&mut self, versions: &[ContainerVersion]) { - if let Some(chain) = &mut self.chain { - for version in versions { - if chain.contains_key(&version.inner) { - continue; - } - - match chain.get_neighbors(&version.inner) { - (None, Some(status)) => match status { - ItemStatus::Addition { .. } => { - chain.insert(version.inner, ItemStatus::NotPresent) - } - ItemStatus::Change { - from_ident, - from_type, - .. - } => chain.insert( - version.inner, - ItemStatus::NoChange { - previously_deprecated: false, - ident: from_ident.clone(), - ty: from_type.clone(), - }, - ), - ItemStatus::Deprecation { previous_ident, .. } => chain.insert( - version.inner, - ItemStatus::NoChange { - previously_deprecated: false, - ident: previous_ident.clone(), - ty: self.inner.ty(), - }, - ), - ItemStatus::NoChange { - previously_deprecated, - ident, - ty, - } => chain.insert( - version.inner, - ItemStatus::NoChange { - previously_deprecated: *previously_deprecated, - ident: ident.clone(), - ty: ty.clone(), - }, - ), - ItemStatus::NotPresent => unreachable!(), - }, - (Some(status), None) => { - let (ident, ty, previously_deprecated) = match status { - ItemStatus::Addition { ident, ty, .. } => (ident, ty, false), - ItemStatus::Change { - to_ident, to_type, .. - } => (to_ident, to_type, false), - ItemStatus::Deprecation { ident, .. } => { - (ident, &self.inner.ty(), true) - } - ItemStatus::NoChange { - previously_deprecated, - ident, - ty, - .. - } => (ident, ty, *previously_deprecated), - ItemStatus::NotPresent => unreachable!(), - }; - - chain.insert( - version.inner, - ItemStatus::NoChange { - previously_deprecated, - ident: ident.clone(), - ty: ty.clone(), - }, - ) - } - (Some(status), Some(_)) => { - let (ident, ty, previously_deprecated) = match status { - ItemStatus::Addition { ident, ty, .. } => (ident, ty, false), - ItemStatus::Change { - to_ident, to_type, .. - } => (to_ident, to_type, false), - ItemStatus::NoChange { - previously_deprecated, - ident, - ty, - .. - } => (ident, ty, *previously_deprecated), - _ => unreachable!(), - }; - - chain.insert( - version.inner, - ItemStatus::NoChange { - previously_deprecated, - ident: ident.clone(), - ty: ty.clone(), - }, - ) - } - _ => unreachable!(), - }; - } - } - } - - fn get_ident(&self, version: &ContainerVersion) -> Option<&Ident> { - match &self.chain { - Some(chain) => chain - .get(&version.inner) - .expect("internal error: chain must contain container version") - .get_ident(), - None => Some(self.inner.ident()), - } - } -} - -#[derive(Debug, PartialEq)] -pub(crate) enum ItemStatus { - Addition { - ident: Ident, - default_fn: Path, - // NOTE (@Techassi): We need to carry idents and type information in - // nearly every status. Ideally, we would store this in separate maps. - ty: Type, - }, - Change { - from_ident: Ident, - to_ident: Ident, - from_type: Type, - to_type: Type, - }, - Deprecation { - previous_ident: Ident, - note: Option, - ident: Ident, - }, - NoChange { - previously_deprecated: bool, - ident: Ident, - ty: Type, - }, - NotPresent, -} - -impl ItemStatus { - pub(crate) fn get_ident(&self) -> Option<&Ident> { - match &self { - ItemStatus::Addition { ident, .. } => Some(ident), - ItemStatus::Change { to_ident, .. } => Some(to_ident), - ItemStatus::Deprecation { ident, .. } => Some(ident), - ItemStatus::NoChange { ident, .. } => Some(ident), - ItemStatus::NotPresent => None, - } - } -} diff --git a/crates/stackable-versioned-macros/src/codegen/common/mod.rs b/crates/stackable-versioned-macros/src/codegen/common/mod.rs deleted file mode 100644 index d46cfd160..000000000 --- a/crates/stackable-versioned-macros/src/codegen/common/mod.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::collections::BTreeMap; - -use k8s_version::Version; -use proc_macro2::Span; -use quote::format_ident; -use syn::Ident; - -use crate::{ - attrs::common::ContainerAttributes, - consts::{DEPRECATED_FIELD_PREFIX, DEPRECATED_VARIANT_PREFIX}, -}; - -mod container; -mod item; - -pub(crate) use container::*; -pub(crate) use item::*; - -/// Type alias to make the type of the version chain easier to handle. -pub(crate) type VersionChain = BTreeMap; - -#[derive(Debug, Clone)] -pub(crate) struct ContainerVersion { - /// Indicates that the container version is deprecated. - pub(crate) deprecated: bool, - - /// Indicates that the generation of `From for NEW` should be skipped. - pub(crate) skip_from: bool, - - /// A validated Kubernetes API version. - pub(crate) inner: Version, - - /// The ident of the container. - pub(crate) ident: Ident, - - /// Store additional doc-comment lines for this version. - pub(crate) version_specific_docs: Vec, -} - -/// Converts lines of doc-comments into a trimmed list. -fn process_docs(input: &Option) -> Vec { - if let Some(input) = input { - input - // Trim the leading and trailing whitespace, deleting suprefluous - // empty lines. - .trim() - .lines() - // Trim the leading and trailing whitespace on each line that can be - // introduced when the developer indents multi-line comments. - .map(|line| line.trim().to_owned()) - .collect() - } else { - Vec::new() - } -} - -impl From<&ContainerAttributes> for Vec { - fn from(attributes: &ContainerAttributes) -> Self { - attributes - .versions - .iter() - .map(|v| ContainerVersion { - skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()), - ident: Ident::new(&v.name.to_string(), Span::call_site()), - deprecated: v.deprecated.is_present(), - inner: v.name, - version_specific_docs: process_docs(&v.doc), - }) - .collect() - } -} - -/// Removes the deprecated prefix from a field ident. -/// -/// See [`DEPRECATED_FIELD_PREFIX`]. -pub(crate) fn remove_deprecated_field_prefix(ident: &Ident) -> Ident { - let ident = ident.to_string(); - let ident = ident.trim_start_matches(DEPRECATED_FIELD_PREFIX); - - format_ident!("{ident}") -} - -/// Removes the deprecated prefix from a variant ident. -/// -/// See [`DEPRECATED_VARIANT_PREFIX`]. -pub(crate) fn remove_deprecated_variant_prefix(ident: &Ident) -> Ident { - // NOTE (@Techassi): Currently Clippy only issues a warning for variants - // with underscores in their name. That's why we additionally remove the - // leading underscore from the ident to use the expected name during code - // generation. - let ident = ident.to_string(); - let ident = ident - .trim_start_matches(DEPRECATED_VARIANT_PREFIX) - .trim_start_matches('_'); - - format_ident!("{ident}") -} diff --git a/crates/stackable-versioned-macros/src/codegen/container/enum.rs b/crates/stackable-versioned-macros/src/codegen/container/enum.rs new file mode 100644 index 000000000..ed2244e63 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/container/enum.rs @@ -0,0 +1,212 @@ +use std::ops::Not; + +use darling::{util::IdentString, FromAttributes, Result}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::ItemEnum; + +use crate::{ + attrs::container::NestedContainerAttributes, + codegen::{ + changes::Neighbors, + container::{CommonContainerData, Container, ContainerIdents, ContainerOptions}, + item::VersionedVariant, + ItemStatus, StandaloneContainerAttributes, VersionDefinition, + }, +}; + +impl Container { + pub(crate) fn new_standalone_enum( + item_enum: ItemEnum, + attributes: StandaloneContainerAttributes, + versions: &[VersionDefinition], + ) -> Result { + let mut versioned_variants = Vec::new(); + for variant in item_enum.variants { + let mut versioned_variant = VersionedVariant::new(variant, versions)?; + versioned_variant.insert_container_versions(versions); + versioned_variants.push(versioned_variant); + } + + let options = ContainerOptions { + kubernetes_options: None, + skip_from: attributes + .common_root_arguments + .options + .skip + .map_or(false, |s| s.from.is_present()), + }; + + let idents: ContainerIdents = item_enum.ident.into(); + + let common = CommonContainerData { + original_attributes: item_enum.attrs, + options, + idents, + }; + + Ok(Self::Enum(Enum { + variants: versioned_variants, + common, + })) + } + + // TODO (@Techassi): See what can be unified into a single 'new' function + pub(crate) fn new_enum_nested( + item_enum: ItemEnum, + versions: &[VersionDefinition], + ) -> Result { + let attributes = NestedContainerAttributes::from_attributes(&item_enum.attrs)?; + + let mut versioned_variants = Vec::new(); + for variant in item_enum.variants { + let mut versioned_variant = VersionedVariant::new(variant, versions)?; + versioned_variant.insert_container_versions(versions); + versioned_variants.push(versioned_variant); + } + + let options = ContainerOptions { + kubernetes_options: None, + skip_from: attributes + .options + .skip + .map_or(false, |s| s.from.is_present()), + }; + + let idents: ContainerIdents = item_enum.ident.into(); + + let common = CommonContainerData { + original_attributes: item_enum.attrs, + options, + idents, + }; + + Ok(Self::Enum(Enum { + variants: versioned_variants, + common, + })) + } +} + +/// A versioned enum. +pub(crate) struct Enum { + /// List of variants defined in the original enum. How, and if, an item + /// should generate code, is decided by the currently generated version. + pub(crate) variants: Vec, + + /// Common container data which is shared between enums and structs. + pub(crate) common: CommonContainerData, +} + +// Common token generation +impl Enum { + /// Generates code for the enum definition. + pub(crate) fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { + let original_attributes = &self.common.original_attributes; + let ident = &self.common.idents.original; + let version_docs = &version.docs; + + let mut variants = TokenStream::new(); + for variant in &self.variants { + variants.extend(variant.generate_for_container(version)); + } + + quote! { + #(#[doc = #version_docs])* + #(#original_attributes)* + pub enum #ident { + #variants + } + } + } + + /// Generates code for the `From for NextVersion` implementation. + pub(crate) fn generate_from_impl( + &self, + version: &VersionDefinition, + next_version: Option<&VersionDefinition>, + is_nested: bool, + ) -> Option { + if version.skip_from || self.common.options.skip_from { + return None; + } + + match next_version { + Some(next_version) => { + let enum_ident = &self.common.idents.original; + let from_ident = &self.common.idents.from; + + let next_version_ident = &next_version.ident; + let version_ident = &version.ident; + + let variants = self.generate_from_variants(version, next_version, enum_ident); + + // Include allow(deprecated) only when this or the next version is + // deprecated. Also include it, when a variant in this or the next + // version is deprecated. + let allow_attribute = (version.deprecated.is_some() + || next_version.deprecated.is_some() + || self.is_any_variant_deprecated(version) + || self.is_any_variant_deprecated(next_version)) + .then_some(quote! { #[allow(deprecated)] }); + + // Only add the #[automatically_derived] attribute only if this impl is used + // outside of a module (in standalone mode). + let automatically_derived = + is_nested.not().then(|| quote! {#[automatically_derived]}); + + Some(quote! { + #automatically_derived + #allow_attribute + impl ::std::convert::From<#version_ident::#enum_ident> for #next_version_ident::#enum_ident { + fn from(#from_ident: #version_ident::#enum_ident) -> Self { + match #from_ident { + #variants + } + } + } + }) + } + None => None, + } + } + + /// Generates code for enum variants used in `From` implementations. + fn generate_from_variants( + &self, + version: &VersionDefinition, + next_version: &VersionDefinition, + enum_ident: &IdentString, + ) -> TokenStream { + let mut tokens = TokenStream::new(); + + for variant in &self.variants { + tokens.extend(variant.generate_for_from_impl(version, next_version, enum_ident)); + } + + tokens + } + + /// Returns whether any variant is deprecated in the provided `version`. + fn is_any_variant_deprecated(&self, version: &VersionDefinition) -> bool { + // First, iterate over all variants. The `any` function will return true + // if any of the function invocations return true. If a variant doesn't + // have a chain, we can safely default to false (unversioned variants + // cannot be deprecated). Then we retrieve the status of the variant and + // ensure it is deprecated. + self.variants.iter().any(|f| { + f.changes.as_ref().map_or(false, |c| { + c.value_is(&version.inner, |a| { + matches!( + a, + ItemStatus::Deprecation { .. } + | ItemStatus::NoChange { + previously_deprecated: true, + .. + } + ) + }) + }) + }) + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/container/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/mod.rs new file mode 100644 index 000000000..cef4eb88d --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/container/mod.rs @@ -0,0 +1,265 @@ +use darling::{util::IdentString, Result}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Attribute, Ident, ItemEnum, ItemStruct, Visibility}; + +use crate::{ + attrs::{container::StandaloneContainerAttributes, k8s::KubernetesArguments}, + codegen::{ + container::{r#enum::Enum, r#struct::Struct}, + VersionDefinition, + }, + utils::ContainerIdentExt, +}; + +mod r#enum; +mod r#struct; + +/// Contains common container data shared between structs and enums. +pub(crate) struct CommonContainerData { + /// Original attributes placed on the container, like `#[derive()]` or `#[cfg()]`. + pub(crate) original_attributes: Vec, + + /// Different options which influence code generation. + pub(crate) options: ContainerOptions, + + /// A collection of container idents used for different purposes. + pub(crate) idents: ContainerIdents, +} + +/// Supported types of containers, structs and enums. +/// +/// Abstracting away with kind of container is generated makes it possible to create a list of +/// containers when the macro is used on modules. This enum provides functions to generate code +/// which then internally call the appropriate function based on the variant. +pub(crate) enum Container { + Struct(Struct), + Enum(Enum), +} + +impl Container { + /// Generates the container definition for the specified `version`. + pub(crate) fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { + match self { + Container::Struct(s) => s.generate_definition(version), + Container::Enum(e) => e.generate_definition(version), + } + } + + /// Generates the container `From for NextVersion` implementation. + pub(crate) fn generate_from_impl( + &self, + version: &VersionDefinition, + next_version: Option<&VersionDefinition>, + add_attributes: bool, + ) -> Option { + match self { + Container::Struct(s) => s.generate_from_impl(version, next_version, add_attributes), + Container::Enum(e) => e.generate_from_impl(version, next_version, add_attributes), + } + } + + /// Generates Kubernetes specific code snippets. + /// + /// This function returns three values: + /// + /// - an enum variant ident, + /// - an enum variant display string, + /// - and a `CustomResource::crd()` call + /// + /// This function only returns `Some` if it is a struct. Enums cannot be used to define + /// Kubernetes custom resources. + pub(crate) fn generate_kubernetes_item( + &self, + version: &VersionDefinition, + ) -> Option<(IdentString, String, TokenStream)> { + match self { + Container::Struct(s) => s.generate_kubernetes_item(version), + Container::Enum(_) => None, + } + } + + /// Generates Kubernetes specific code to merge two or more CRDs into one. + /// + /// This function only returns `Some` if it is a struct. Enums cannot be used to define + /// Kubernetes custom resources. + pub(crate) fn generate_kubernetes_merge_crds( + &self, + enum_variant_idents: Vec, + enum_variant_strings: Vec, + fn_calls: Vec, + is_nested: bool, + ) -> Option { + match self { + Container::Struct(s) => s.generate_kubernetes_merge_crds( + enum_variant_idents, + enum_variant_strings, + fn_calls, + is_nested, + ), + Container::Enum(_) => None, + } + } +} + +/// A versioned standalone container. +/// +/// A standalone container is a container defined outside of a versioned module. See [`Module`][1] +/// for more information about versioned modules. +/// +/// [1]: crate::codegen::module::Module +pub(crate) struct StandaloneContainer { + versions: Vec, + container: Container, + vis: Visibility, +} + +impl StandaloneContainer { + /// Creates a new versioned standalone struct. + pub(crate) fn new_struct( + item_struct: ItemStruct, + attributes: StandaloneContainerAttributes, + ) -> Result { + let versions: Vec<_> = (&attributes).into(); + let vis = item_struct.vis.clone(); + + let container = Container::new_standalone_struct(item_struct, attributes, &versions)?; + + Ok(Self { + container, + versions, + vis, + }) + } + + /// Creates a new versioned standalone enum. + pub(crate) fn new_enum( + item_enum: ItemEnum, + attributes: StandaloneContainerAttributes, + ) -> Result { + let versions: Vec<_> = (&attributes).into(); + let vis = item_enum.vis.clone(); + + let container = Container::new_standalone_enum(item_enum, attributes, &versions)?; + + Ok(Self { + container, + versions, + vis, + }) + } + + /// Generate tokens containing every piece of code required for a standalone container. + pub(crate) fn generate_tokens(&self) -> TokenStream { + let vis = &self.vis; + + let mut tokens = TokenStream::new(); + + let mut kubernetes_merge_crds_fn_calls = Vec::new(); + let mut kubernetes_enum_variant_idents = Vec::new(); + let mut kubernetes_enum_variant_strings = Vec::new(); + + let mut versions = self.versions.iter().peekable(); + + while let Some(version) = versions.next() { + let container_definition = self.container.generate_definition(version); + + // NOTE (@Techassi): Using '.copied()' here does not copy or clone the data, but instead + // removes one level of indirection of the double reference '&&'. + let from_impl = + self.container + .generate_from_impl(version, versions.peek().copied(), false); + + // Add the #[deprecated] attribute when the version is marked as deprecated. + let deprecated_attribute = version + .deprecated + .as_ref() + .map(|note| quote! { #[deprecated = #note] }); + + // Generate Kubernetes specific code which is placed outside of the container + // definition. + if let Some((enum_variant_ident, enum_variant_string, fn_call)) = + self.container.generate_kubernetes_item(version) + { + kubernetes_merge_crds_fn_calls.push(fn_call); + kubernetes_enum_variant_idents.push(enum_variant_ident); + kubernetes_enum_variant_strings.push(enum_variant_string); + } + + let version_ident = &version.ident; + + tokens.extend(quote! { + #[automatically_derived] + #deprecated_attribute + #vis mod #version_ident { + use super::*; + #container_definition + } + + #from_impl + }); + } + + tokens.extend(self.container.generate_kubernetes_merge_crds( + kubernetes_enum_variant_idents, + kubernetes_enum_variant_strings, + kubernetes_merge_crds_fn_calls, + false, + )); + + tokens + } +} + +/// A collection of container idents used for different purposes. +#[derive(Debug)] +pub(crate) struct ContainerIdents { + /// The ident used in the context of Kubernetes specific code. This ident + /// removes the 'Spec' suffix present in the definition container. + pub(crate) kubernetes: IdentString, + + /// The original ident, or name, of the versioned container. + pub(crate) original: IdentString, + + /// The ident used in the [`From`] impl. + pub(crate) from: IdentString, +} + +impl From for ContainerIdents { + fn from(ident: Ident) -> Self { + Self { + kubernetes: ident.as_cleaned_kubernetes_ident(), + from: ident.as_from_impl_ident(), + original: ident.into(), + } + } +} + +#[derive(Debug)] +pub(crate) struct ContainerOptions { + pub(crate) kubernetes_options: Option, + pub(crate) skip_from: bool, +} + +#[derive(Debug)] +pub(crate) struct KubernetesOptions { + pub(crate) singular: Option, + pub(crate) plural: Option, + pub(crate) skip_merged_crd: bool, + pub(crate) kind: Option, + pub(crate) namespaced: bool, + pub(crate) group: String, +} + +impl From for KubernetesOptions { + fn from(args: KubernetesArguments) -> Self { + KubernetesOptions { + skip_merged_crd: args.skip.map_or(false, |s| s.merged_crd.is_present()), + namespaced: args.namespaced.is_present(), + singular: args.singular, + plural: args.plural, + group: args.group, + kind: args.kind, + } + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct.rs b/crates/stackable-versioned-macros/src/codegen/container/struct.rs new file mode 100644 index 000000000..fcf7ee368 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/container/struct.rs @@ -0,0 +1,399 @@ +use std::ops::Not; + +use darling::{util::IdentString, Error, FromAttributes, Result}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{parse_quote, ItemStruct, Path}; + +use crate::{ + attrs::container::NestedContainerAttributes, + codegen::{ + changes::Neighbors, + container::{CommonContainerData, Container, ContainerIdents, ContainerOptions}, + item::VersionedField, + ItemStatus, StandaloneContainerAttributes, VersionDefinition, + }, + utils::VersionExt, +}; + +impl Container { + pub(crate) fn new_standalone_struct( + item_struct: ItemStruct, + attributes: StandaloneContainerAttributes, + versions: &[VersionDefinition], + ) -> Result { + // NOTE (@Techassi): Should we check if the fields are named here? + let mut versioned_fields = Vec::new(); + + for field in item_struct.fields { + let mut versioned_field = VersionedField::new(field, versions)?; + versioned_field.insert_container_versions(versions); + versioned_fields.push(versioned_field); + } + + let kubernetes_options = attributes.kubernetes_arguments.map(Into::into); + let idents: ContainerIdents = item_struct.ident.into(); + + // Validate K8s specific requirements + // Ensure that the struct name includes the 'Spec' suffix. + if kubernetes_options.is_some() && !idents.original.as_str().ends_with("Spec") { + return Err(Error::custom( + "struct name needs to include the `Spec` suffix if Kubernetes features are enabled via `#[versioned(k8s())]`" + ).with_span(&idents.original.span())); + } + + let options = ContainerOptions { + skip_from: attributes + .common_root_arguments + .options + .skip + .map_or(false, |s| s.from.is_present()), + kubernetes_options, + }; + + let common = CommonContainerData { + original_attributes: item_struct.attrs, + options, + idents, + }; + + Ok(Self::Struct(Struct { + fields: versioned_fields, + common, + })) + } + + // TODO (@Techassi): See what can be unified into a single 'new' function + pub(crate) fn new_struct_nested( + item_struct: ItemStruct, + versions: &[VersionDefinition], + ) -> Result { + let attributes = NestedContainerAttributes::from_attributes(&item_struct.attrs)?; + + let mut versioned_fields = Vec::new(); + for field in item_struct.fields { + let mut versioned_field = VersionedField::new(field, versions)?; + versioned_field.insert_container_versions(versions); + versioned_fields.push(versioned_field); + } + + let kubernetes_options = attributes.kubernetes_arguments.map(Into::into); + let idents: ContainerIdents = item_struct.ident.into(); + + // Validate K8s specific requirements + // Ensure that the struct name includes the 'Spec' suffix. + if kubernetes_options.is_some() && !idents.original.as_str().ends_with("Spec") { + return Err(Error::custom( + "struct name needs to include the `Spec` suffix if Kubernetes features are enabled via `#[versioned(k8s())]`" + ).with_span(&idents.original.span())); + } + + let options = ContainerOptions { + skip_from: attributes + .options + .skip + .map_or(false, |s| s.from.is_present()), + kubernetes_options, + }; + + // Nested structs + // We need to filter out the `versioned` attribute, because these are not directly processed + // by darling, but instead by us (using darling). For this reason, darling won't remove the + // attribute from the input and as such, we need to filter it out ourself. + let original_attributes = item_struct + .attrs + .into_iter() + .filter(|attr| !attr.meta.path().is_ident("versioned")) + .collect(); + + let common = CommonContainerData { + original_attributes, + options, + idents, + }; + + Ok(Self::Struct(Struct { + fields: versioned_fields, + common, + })) + } +} + +/// A versioned struct. +pub(crate) struct Struct { + /// List of fields defined in the original struct. How, and if, an item + /// should generate code, is decided by the currently generated version. + pub(crate) fields: Vec, + + /// Common container data which is shared between structs and enums. + pub(crate) common: CommonContainerData, +} + +// Common token generation +impl Struct { + /// Generates code for the struct definition. + pub(crate) fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { + let original_attributes = &self.common.original_attributes; + let ident = &self.common.idents.original; + let version_docs = &version.docs; + + let mut fields = TokenStream::new(); + for field in &self.fields { + fields.extend(field.generate_for_container(version)); + } + + // This only returns Some, if K8s features are enabled + let kubernetes_cr_derive = self.generate_kubernetes_cr_derive(version); + + quote! { + #(#[doc = #version_docs])* + #(#original_attributes)* + #kubernetes_cr_derive + pub struct #ident { + #fields + } + } + } + + /// Generates code for the `From for NextVersion` implementation. + pub(crate) fn generate_from_impl( + &self, + version: &VersionDefinition, + next_version: Option<&VersionDefinition>, + add_attributes: bool, + ) -> Option { + if version.skip_from || self.common.options.skip_from { + return None; + } + + match next_version { + Some(next_version) => { + let struct_ident = &self.common.idents.original; + let from_struct_ident = &self.common.idents.from; + + let for_module_ident = &next_version.ident; + let from_module_ident = &version.ident; + + let fields = self.generate_from_fields(version, next_version, from_struct_ident); + + // Include allow(deprecated) only when this or the next version is + // deprecated. Also include it, when a field in this or the next + // version is deprecated. + let allow_attribute = (version.deprecated.is_some() + || next_version.deprecated.is_some() + || self.is_any_field_deprecated(version) + || self.is_any_field_deprecated(next_version)) + .then(|| quote! { #[allow(deprecated)] }); + + // Only add the #[automatically_derived] attribute only if this impl is used + // outside of a module (in standalone mode). + let automatically_derived = add_attributes + .not() + .then(|| quote! {#[automatically_derived]}); + + Some(quote! { + #automatically_derived + #allow_attribute + impl ::std::convert::From<#from_module_ident::#struct_ident> for #for_module_ident::#struct_ident { + fn from(#from_struct_ident: #from_module_ident::#struct_ident) -> Self { + Self { + #fields + } + } + } + }) + } + None => None, + } + } + + /// Generates code for struct fields used in `From` implementations. + fn generate_from_fields( + &self, + version: &VersionDefinition, + next_version: &VersionDefinition, + from_struct_ident: &IdentString, + ) -> TokenStream { + let mut tokens = TokenStream::new(); + + for field in &self.fields { + tokens.extend(field.generate_for_from_impl(version, next_version, from_struct_ident)); + } + + tokens + } + + /// Returns whether any field is deprecated in the provided `version`. + fn is_any_field_deprecated(&self, version: &VersionDefinition) -> bool { + // First, iterate over all fields. The `any` function will return true + // if any of the function invocations return true. If a field doesn't + // have a chain, we can safely default to false (unversioned fields + // cannot be deprecated). Then we retrieve the status of the field and + // ensure it is deprecated. + self.fields.iter().any(|f| { + f.changes.as_ref().map_or(false, |c| { + c.value_is(&version.inner, |a| { + matches!( + a, + ItemStatus::Deprecation { .. } + | ItemStatus::NoChange { + previously_deprecated: true, + .. + } + ) + }) + }) + }) + } +} + +// Kubernetes-specific token generation +impl Struct { + pub(crate) fn generate_kubernetes_cr_derive( + &self, + version: &VersionDefinition, + ) -> Option { + match &self.common.options.kubernetes_options { + Some(kubernetes_options) => { + // Required arguments + let group = &kubernetes_options.group; + let version = version.inner.to_string(); + let kind = kubernetes_options + .kind + .as_ref() + .map_or(self.common.idents.kubernetes.to_string(), |kind| { + kind.clone() + }); + + // Optional arguments + let namespaced = kubernetes_options + .namespaced + .then_some(quote! { , namespaced }); + let singular = kubernetes_options + .singular + .as_ref() + .map(|s| quote! { , singular = #s }); + let plural = kubernetes_options + .plural + .as_ref() + .map(|p| quote! { , plural = #p }); + + Some(quote! { + #[derive(::kube::CustomResource)] + #[kube(group = #group, version = #version, kind = #kind #singular #plural #namespaced)] + }) + } + None => None, + } + } + + pub(crate) fn generate_kubernetes_item( + &self, + version: &VersionDefinition, + ) -> Option<(IdentString, String, TokenStream)> { + match &self.common.options.kubernetes_options { + Some(options) if !options.skip_merged_crd => { + let enum_variant_ident = version.inner.as_variant_ident(); + let enum_variant_string = version.inner.to_string(); + + let struct_ident = &self.common.idents.kubernetes; + let module_ident = &version.ident; + let qualified_path: Path = parse_quote!(#module_ident::#struct_ident); + + let merge_crds_fn_call = quote! { + <#qualified_path as ::kube::CustomResourceExt>::crd() + }; + + Some((enum_variant_ident, enum_variant_string, merge_crds_fn_call)) + } + _ => None, + } + } + + pub(crate) fn generate_kubernetes_merge_crds( + &self, + enum_variant_idents: Vec, + enum_variant_strings: Vec, + fn_calls: Vec, + is_nested: bool, + ) -> Option { + if enum_variant_idents.is_empty() { + return None; + } + + let enum_ident = &self.common.idents.kubernetes; + + // Only add the #[automatically_derived] attribute if this impl is used outside of a + // module (in standalone mode). + let automatically_derived = is_nested.not().then(|| quote! {#[automatically_derived]}); + + // TODO (@Techassi): Use proper visibility instead of hard-coding 'pub' + // TODO (@Techassi): Move the YAML printing code into 'stackable-versioned' so that we don't + // have any cross-dependencies and the macro can be used on it's own (K8s features of course + // still need kube and friends). + Some(quote! { + #automatically_derived + pub enum #enum_ident { + #(#enum_variant_idents),* + } + + #automatically_derived + impl ::std::fmt::Display for #enum_ident { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { + match self { + #(Self::#enum_variant_idents => f.write_str(#enum_variant_strings)),* + } + } + } + + #automatically_derived + impl #enum_ident { + /// Generates a merged CRD which contains all versions defined using the `#[versioned()]` macro. + pub fn merged_crd( + stored_apiversion: Self + ) -> ::std::result::Result<::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError> { + ::kube::core::crd::merge_crds(vec![#(#fn_calls),*], &stored_apiversion.to_string()) + } + + /// Generates and writes a merged CRD which contains all versions defined using the `#[versioned()]` + /// macro to a file located at `path`. + pub fn write_merged_crd

(path: P, stored_apiversion: Self, operator_version: &str) -> Result<(), ::stackable_versioned::Error> + where P: AsRef<::std::path::Path> + { + use ::stackable_shared::yaml::{YamlSchema, SerializeOptions}; + + let merged_crd = Self::merged_crd(stored_apiversion).map_err(|err| ::stackable_versioned::Error::MergeCrd { + source: err, + })?; + + YamlSchema::write_yaml_schema( + &merged_crd, + path, + operator_version, + SerializeOptions::default() + ).map_err(|err| ::stackable_versioned::Error::SerializeYaml { + source: err, + }) + } + + /// Generates and writes a merged CRD which contains all versions defined using the `#[versioned()]` + /// macro to stdout. + pub fn print_merged_crd(stored_apiversion: Self, operator_version: &str) -> Result<(), ::stackable_versioned::Error> { + use ::stackable_shared::yaml::{YamlSchema, SerializeOptions}; + + let merged_crd = Self::merged_crd(stored_apiversion).map_err(|err| ::stackable_versioned::Error::MergeCrd { + source: err, + })?; + + YamlSchema::print_yaml_schema( + &merged_crd, + operator_version, + SerializeOptions::default() + ).map_err(|err| ::stackable_versioned::Error::SerializeYaml { + source: err, + }) + } + } + }) + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/vstruct/field.rs b/crates/stackable-versioned-macros/src/codegen/item/field.rs similarity index 55% rename from crates/stackable-versioned-macros/src/codegen/vstruct/field.rs rename to crates/stackable-versioned-macros/src/codegen/item/field.rs index 191303915..77a555038 100644 --- a/crates/stackable-versioned-macros/src/codegen/vstruct/field.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/field.rs @@ -1,109 +1,63 @@ -use std::ops::{Deref, DerefMut}; +use std::collections::BTreeMap; -use darling::FromField; +use darling::{util::IdentString, FromField, Result}; +use k8s_version::Version; use proc_macro2::TokenStream; use quote::quote; -use syn::{Field, Ident}; +use syn::{Attribute, Field, Type}; use crate::{ - attrs::{ - common::{ContainerAttributes, ItemAttributes}, - field::FieldAttributes, - }, - codegen::common::{ - remove_deprecated_field_prefix, Attributes, ContainerVersion, InnerItem, Item, ItemStatus, - Named, VersionedItem, + attrs::item::FieldAttributes, + codegen::{ + changes::{BTreeMapExt, ChangesetExt}, + ItemStatus, VersionDefinition, }, + utils::FieldIdent, }; -/// A versioned field, which contains common [`Field`] data and a chain of -/// actions. -/// -/// The chain of actions maps versions to an action and the appropriate field -/// name. -/// -/// Additionally, the [`Field`] data can be used to forward attributes, generate -/// documentation, etc. -#[derive(Debug)] -pub(crate) struct VersionedField(VersionedItem); - -impl Deref for VersionedField { - type Target = VersionedItem; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for VersionedField { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl TryFrom<&Field> for FieldAttributes { - type Error = darling::Error; - - fn try_from(field: &Field) -> Result { - Self::from_field(field) - } -} - -impl Attributes for FieldAttributes { - fn common_attributes_owned(self) -> ItemAttributes { - self.common - } - - fn common_attributes(&self) -> &ItemAttributes { - &self.common - } - - fn original_attributes(&self) -> &Vec { - &self.attrs - } -} - -impl InnerItem for Field { - fn ty(&self) -> syn::Type { - self.ty.clone() - } +pub(crate) struct VersionedField { + pub(crate) original_attributes: Vec, + pub(crate) changes: Option>, + pub(crate) ident: FieldIdent, + pub(crate) ty: Type, } -impl Named for Field { - fn cleaned_ident(&self) -> Ident { - let ident = self.ident(); - remove_deprecated_field_prefix(ident) - } - - fn ident(&self) -> &Ident { - self.ident - .as_ref() - .expect("internal error: field must have an ident") +impl VersionedField { + pub(crate) fn new(field: Field, versions: &[VersionDefinition]) -> Result { + let field_attributes = FieldAttributes::from_field(&field)?; + field_attributes.validate_versions(versions)?; + + let field_ident = FieldIdent::from( + field + .ident + .expect("internal error: field must have an ident"), + ); + let changes = field_attributes + .common + .into_changeset(&field_ident, field.ty.clone()); + + Ok(Self { + original_attributes: field_attributes.attrs, + ident: field_ident, + ty: field.ty, + changes, + }) } -} -impl VersionedField { - /// Creates a new versioned field. - /// - /// Internally this calls [`VersionedItem::new`] to handle most of the - /// common creation code. - pub(crate) fn new( - field: Field, - container_attributes: &ContainerAttributes, - ) -> syn::Result { - let item = VersionedItem::<_, FieldAttributes>::new(field, container_attributes)?; - Ok(Self(item)) + pub(crate) fn insert_container_versions(&mut self, versions: &[VersionDefinition]) { + if let Some(changes) = &mut self.changes { + changes.insert_container_versions(versions, &self.ty); + } } - /// Generates tokens to be used in a container definition. pub(crate) fn generate_for_container( &self, - container_version: &ContainerVersion, + version: &VersionDefinition, ) -> Option { let original_attributes = &self.original_attributes; - match &self.chain { - Some(chain) => { + match &self.changes { + Some(changes) => { // 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 @@ -112,13 +66,13 @@ impl VersionedField { // The code generation then depends on the relation to other // versions (with actions). - let field_type = &self.inner.ty; + let field_type = &self.ty; // NOTE (@Techassi): https://rust-lang.github.io/rust-clippy/master/index.html#/expect_fun_call - match chain.get(&container_version.inner).unwrap_or_else(|| { + match changes.get(&version.inner).unwrap_or_else(|| { panic!( "internal error: chain must contain container version {}", - container_version.inner + version.inner ) }) { ItemStatus::Addition { ident, ty, .. } => Some(quote! { @@ -178,8 +132,8 @@ impl VersionedField { None => { // If there is no chain of field actions, the field is not // versioned and therefore included in all versions. - let field_ident = &self.inner.ident; - let field_type = &self.inner.ty; + let field_ident = &self.ident; + let field_type = &self.ty; Some(quote! { #(#original_attributes)* @@ -189,23 +143,18 @@ impl VersionedField { } } - /// Generates tokens to be used in a [`From`] implementation. pub(crate) fn generate_for_from_impl( &self, - version: &ContainerVersion, - next_version: &ContainerVersion, - from_ident: &Ident, + version: &VersionDefinition, + next_version: &VersionDefinition, + from_struct_ident: &IdentString, ) -> 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"), - ) { + match &self.changes { + Some(changes) => { + let next_change = changes.get_expect(&next_version.inner); + let change = changes.get_expect(&version.inner); + + match (change, next_change) { ( _, ItemStatus::Addition { @@ -225,33 +174,29 @@ impl VersionedField { ) => { if from_type == to_type { quote! { - #to_ident: #from_ident.#old_field_ident, + #to_ident: #from_struct_ident.#old_field_ident, } } else { quote! { - #to_ident: #from_ident.#old_field_ident.into(), + #to_ident: #from_struct_ident.#old_field_ident.into(), } } } (old, next) => { - let old_field_ident = old - .get_ident() - .expect("internal error: old field must have a name"); - - let next_field_ident = next - .get_ident() - .expect("internal error: new field must have a name"); + let next_field_ident = next.get_ident(); + let old_field_ident = old.get_ident(); quote! { - #next_field_ident: #from_ident.#old_field_ident, + #next_field_ident: #from_struct_ident.#old_field_ident, } } } } None => { - let field_ident = &self.inner.ident; + let field_ident = &*self.ident; + quote! { - #field_ident: #from_ident.#field_ident, + #field_ident: #from_struct_ident.#field_ident, } } } diff --git a/crates/stackable-versioned-macros/src/codegen/item/mod.rs b/crates/stackable-versioned-macros/src/codegen/item/mod.rs new file mode 100644 index 000000000..2c1d5f151 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/item/mod.rs @@ -0,0 +1,5 @@ +mod field; +pub(crate) use field::*; + +mod variant; +pub(crate) use variant::*; diff --git a/crates/stackable-versioned-macros/src/codegen/item/variant.rs b/crates/stackable-versioned-macros/src/codegen/item/variant.rs new file mode 100644 index 000000000..db42da520 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/item/variant.rs @@ -0,0 +1,216 @@ +use std::collections::BTreeMap; + +use darling::{util::IdentString, FromVariant, Result}; +use k8s_version::Version; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote}; +use syn::{token::Not, Attribute, Fields, Type, TypeNever, Variant}; + +use crate::{ + attrs::item::VariantAttributes, + codegen::{ + changes::{BTreeMapExt, ChangesetExt}, + ItemStatus, VersionDefinition, + }, + utils::VariantIdent, +}; + +pub(crate) struct VersionedVariant { + pub(crate) original_attributes: Vec, + pub(crate) changes: Option>, + pub(crate) ident: VariantIdent, + pub(crate) fields: Fields, +} + +impl VersionedVariant { + pub(crate) fn new(variant: Variant, versions: &[VersionDefinition]) -> Result { + let variant_attributes = VariantAttributes::from_variant(&variant)?; + variant_attributes.validate_versions(versions)?; + + let variant_ident = VariantIdent::from(variant.ident); + + // FIXME (@Techassi): The chain of changes currently doesn't track versioning of variant + // date and as such, we just use the never type here. During codegen, we just re-emit the + // variant data as is. + let ty = Type::Never(TypeNever { + bang_token: Not([Span::call_site()]), + }); + let changes = variant_attributes.common.into_changeset(&variant_ident, ty); + + Ok(Self { + original_attributes: variant_attributes.attrs, + fields: variant.fields, + ident: variant_ident, + changes, + }) + } + + pub(crate) fn insert_container_versions(&mut self, versions: &[VersionDefinition]) { + if let Some(changes) = &mut self.changes { + // FIXME (@Techassi): Support enum variants with data + let ty = Type::Never(TypeNever { + bang_token: Not([Span::call_site()]), + }); + + changes.insert_container_versions(versions, &ty); + } + } + + /// Generates tokens to be used in a container definition. + pub(crate) fn generate_for_container( + &self, + version: &VersionDefinition, + ) -> Option { + let original_attributes = &self.original_attributes; + let fields = &self.fields; + + match &self.changes { + // NOTE (@Techassi): `unwrap_or_else` used instead of `expect`. + // See: https://rust-lang.github.io/rust-clippy/master/index.html#/expect_fun_call + Some(changes) => match changes.get(&version.inner).unwrap_or_else(|| { + panic!( + "internal error: chain must contain container version {}", + version.inner + ) + }) { + ItemStatus::Addition { ident, .. } => Some(quote! { + #(#original_attributes)* + #ident #fields, + }), + ItemStatus::Change { to_ident, .. } => Some(quote! { + #(#original_attributes)* + #to_ident #fields, + }), + ItemStatus::Deprecation { ident, note, .. } => { + // FIXME (@Techassi): Emitting the deprecated attribute + // should cary over even when the item status is + // 'NoChange'. + // TODO (@Techassi): Make the generation of deprecated + // items customizable. When a container is used as a K8s + // CRD, the item must continue to exist, even when + // deprecated. For other versioning use-cases, that + // might not be the case. + let deprecated_attr = if let Some(note) = note { + quote! {#[deprecated = #note]} + } else { + quote! {#[deprecated]} + }; + + Some(quote! { + #(#original_attributes)* + #deprecated_attr + #ident #fields, + }) + } + ItemStatus::NoChange { + previously_deprecated, + ident, + .. + } => { + // TODO (@Techassi): Also carry along the deprecation + // note. + let deprecated_attr = previously_deprecated.then(|| quote! {#[deprecated]}); + + Some(quote! { + #(#original_attributes)* + #deprecated_attr + #ident #fields, + }) + } + ItemStatus::NotPresent => None, + }, + None => { + // If there is no chain of variant actions, the variant is not + // versioned and code generation is straight forward. + // Unversioned variants are always included in versioned enums. + let ident = &self.ident; + + Some(quote! { + #(#original_attributes)* + #ident #fields, + }) + } + } + } + + pub(crate) fn generate_for_from_impl( + &self, + version: &VersionDefinition, + next_version: &VersionDefinition, + enum_ident: &IdentString, + ) -> Option { + let variant_fields = match &self.fields { + Fields::Named(fields_named) => { + let fields: Vec<_> = fields_named + .named + .iter() + .map(|field| { + field + .ident + .as_ref() + .expect("named fields always have an ident") + .clone() + }) + .collect(); + + quote! { { #(#fields),* } } + } + Fields::Unnamed(fields_unnamed) => { + let fields: Vec<_> = fields_unnamed + .unnamed + .iter() + .enumerate() + .map(|(index, _)| format_ident!("__sv_{index}")) + .collect(); + + quote! { ( #(#fields),* ) } + } + Fields::Unit => TokenStream::new(), + }; + + match &self.changes { + Some(changes) => { + let next_change = changes.get_expect(&next_version.inner); + let change = changes.get_expect(&version.inner); + + match (change, next_change) { + (_, ItemStatus::Addition { .. }) => None, + (old, next) => { + let next_version_ident = &next_version.ident; + let old_version_ident = &version.ident; + + let next_variant_ident = next.get_ident(); + let old_variant_ident = old.get_ident(); + + let old = quote! { + #old_version_ident::#enum_ident::#old_variant_ident #variant_fields + }; + let next = quote! { + #next_version_ident::#enum_ident::#next_variant_ident #variant_fields + }; + + Some(quote! { + #old => #next, + }) + } + } + } + None => { + let next_version_ident = &next_version.ident; + let old_version_ident = &version.ident; + let variant_ident = &*self.ident; + + let old = quote! { + #old_version_ident::#enum_ident::#variant_ident #variant_fields + }; + let next = quote! { + #next_version_ident::#enum_ident::#variant_ident #variant_fields + }; + + Some(quote! { + #old => #next, + }) + } + } + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/mod.rs b/crates/stackable-versioned-macros/src/codegen/mod.rs index f1b1bab79..8b2c8dba9 100644 --- a/crates/stackable-versioned-macros/src/codegen/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/mod.rs @@ -1,59 +1,129 @@ -use proc_macro2::TokenStream; -use syn::{spanned::Spanned, Data, DeriveInput, Error, Result}; - -use crate::{ - attrs::common::ContainerAttributes, - codegen::{ - common::{Container, ContainerInput}, - venum::VersionedEnum, - vstruct::VersionedStruct, +use darling::util::IdentString; +use k8s_version::Version; +use quote::format_ident; +use syn::{Path, Type}; + +use crate::attrs::{container::StandaloneContainerAttributes, module::ModuleAttributes}; + +pub(crate) mod changes; +pub(crate) mod container; +pub(crate) mod item; +pub(crate) mod module; + +#[derive(Debug)] +pub(crate) struct VersionDefinition { + /// Indicates that the container version is deprecated. + pub(crate) deprecated: Option, + + /// Indicates that the generation of `From for NEW` should be skipped. + pub(crate) skip_from: bool, + + /// A validated Kubernetes API version. + pub(crate) inner: Version, + + /// The ident of the container. + pub(crate) ident: IdentString, + + /// Store additional doc-comment lines for this version. + pub(crate) docs: Vec, +} + +// NOTE (@Techassi): Can we maybe unify these two impls? +impl From<&StandaloneContainerAttributes> for Vec { + fn from(attributes: &StandaloneContainerAttributes) -> Self { + attributes + .common_root_arguments + .versions + .iter() + .map(|v| VersionDefinition { + skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()), + ident: format_ident!("{version}", version = v.name.to_string()).into(), + deprecated: v.deprecated.as_ref().map(|r#override| { + r#override + .clone() + .unwrap_or(format!("Version {version} is deprecated", version = v.name)) + }), + docs: process_docs(&v.doc), + inner: v.name, + }) + .collect() + } +} + +impl From<&ModuleAttributes> for Vec { + fn from(attributes: &ModuleAttributes) -> Self { + attributes + .common_root_arguments + .versions + .iter() + .map(|v| VersionDefinition { + skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()), + ident: format_ident!("{version}", version = v.name.to_string()).into(), + deprecated: v.deprecated.as_ref().map(|r#override| { + r#override + .clone() + .unwrap_or(format!("Version {version} is deprecated", version = v.name)) + }), + docs: process_docs(&v.doc), + inner: v.name, + }) + .collect() + } +} + +#[derive(Debug, PartialEq)] +pub(crate) enum ItemStatus { + Addition { + ident: IdentString, + default_fn: Path, + // NOTE (@Techassi): We need to carry idents and type information in + // nearly every status. Ideally, we would store this in separate maps. + ty: Type, }, -}; - -pub(crate) mod chain; -pub(crate) mod common; -pub(crate) mod venum; -pub(crate) mod vstruct; - -// NOTE (@Techassi): This derive macro cannot handle multiple structs / enums -// to be versioned within the same file. This is because we cannot declare -// modules more than once (They will not be merged, like impl blocks for -// example). This leads to collisions if there are multiple structs / enums -// which declare the same version. This could maybe be solved by using an -// attribute macro applied to a module with all struct / enums declared in said -// module. This would allow us to generate all versioned structs and enums in -// a single sweep and put them into the appropriate module. - -// TODO (@Techassi): Think about how we can handle nested structs / enums which -// are also versioned. - -pub(crate) fn expand(attributes: ContainerAttributes, input: DeriveInput) -> Result { - let expanded = match input.data { - Data::Struct(data) => { - let input = ContainerInput { - original_attributes: input.attrs, - visibility: input.vis, - ident: input.ident, - }; - - VersionedStruct::new(input, data, attributes)?.generate_tokens() - } - Data::Enum(data) => { - let input = ContainerInput { - original_attributes: input.attrs, - visibility: input.vis, - ident: input.ident, - }; - - VersionedEnum::new(input, data, attributes)?.generate_tokens() - } - _ => { - return Err(Error::new( - input.span(), - "attribute macro `versioned` only supports structs and enums", - )) + Change { + from_ident: IdentString, + to_ident: IdentString, + from_type: Type, + to_type: Type, + }, + Deprecation { + previous_ident: IdentString, + note: Option, + ident: IdentString, + }, + NoChange { + previously_deprecated: bool, + ident: IdentString, + ty: Type, + }, + NotPresent, +} + +impl ItemStatus { + pub(crate) fn get_ident(&self) -> &IdentString { + match &self { + ItemStatus::Addition { ident, .. } => ident, + ItemStatus::Change { to_ident, .. } => to_ident, + ItemStatus::Deprecation { ident, .. } => ident, + ItemStatus::NoChange { ident, .. } => ident, + ItemStatus::NotPresent => unreachable!(), } - }; + } +} - Ok(expanded) +/// Converts lines of doc-comments into a trimmed list. +fn process_docs(input: &Option) -> Vec { + if let Some(input) = input { + input + // Trim the leading and trailing whitespace, deleting superfluous + // empty lines. + .trim() + .lines() + // Trim the leading and trailing whitespace on each line that can be + // introduced when the developer indents multi-line comments. + .map(|line| line.trim().to_owned()) + .collect() + } else { + Vec::new() + } } diff --git a/crates/stackable-versioned-macros/src/codegen/module.rs b/crates/stackable-versioned-macros/src/codegen/module.rs new file mode 100644 index 000000000..be2bcc1f3 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/module.rs @@ -0,0 +1,119 @@ +use std::ops::Not; + +use darling::util::IdentString; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{token::Pub, Ident, Visibility}; + +use crate::codegen::{container::Container, VersionDefinition}; + +pub(crate) struct ModuleInput { + pub(crate) vis: Visibility, + pub(crate) ident: Ident, +} + +/// A versioned module. +/// +/// Versioned modules allow versioning multiple containers at once without introducing conflicting +/// version module definitions. +pub(crate) struct Module { + versions: Vec, + containers: Vec, + preserve_module: bool, + ident: IdentString, + vis: Visibility, +} + +impl Module { + /// Creates a new versioned module containing versioned containers. + pub(crate) fn new( + ModuleInput { ident, vis, .. }: ModuleInput, + preserve_module: bool, + versions: Vec, + containers: Vec, + ) -> Self { + Self { + ident: ident.into(), + preserve_module, + containers, + versions, + vis, + } + } + + /// Generates tokens for all versioned containers. + pub(crate) fn generate_tokens(&self) -> TokenStream { + if self.containers.is_empty() { + return quote! {}; + } + + // If the 'preserve_module' flag is provided by the user, we need to change the visibility + // of version modules (eg. 'v1alpha1') to be public, so that they are accessible inside the + // preserved (wrapping) module. Otherwise, we can inherit the visibility from the module + // which will be erased. + let version_module_vis = if self.preserve_module { + &Visibility::Public(Pub::default()) + } else { + &self.vis + }; + + let mut tokens = TokenStream::new(); + + let module_ident = &self.ident; + let module_vis = &self.vis; + + let mut versions = self.versions.iter().peekable(); + + while let Some(version) = versions.next() { + let mut container_definitions = TokenStream::new(); + let mut from_impls = TokenStream::new(); + + let version_ident = &version.ident; + + for container in &self.containers { + container_definitions.extend(container.generate_definition(version)); + from_impls.extend(container.generate_from_impl( + version, + versions.peek().copied(), + self.preserve_module, + )); + } + + // Only add #[automatically_derived] here if the user doesn't want to preserve the + // module. + let automatically_derived = self + .preserve_module + .not() + .then(|| quote! {#[automatically_derived]}); + + // Add the #[deprecated] attribute when the version is marked as deprecated. + let deprecated_attribute = version + .deprecated + .as_ref() + .map(|note| quote! { #[deprecated = #note] }); + + tokens.extend(quote! { + #automatically_derived + #deprecated_attribute + #version_module_vis mod #version_ident { + use super::*; + + #container_definitions + } + + #from_impls + }); + } + + if self.preserve_module { + quote! { + #[automatically_derived] + #module_vis mod #module_ident { + #tokens + } + } + } else { + tokens + } + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/venum/mod.rs b/crates/stackable-versioned-macros/src/codegen/venum/mod.rs deleted file mode 100644 index 14092e0b8..000000000 --- a/crates/stackable-versioned-macros/src/codegen/venum/mod.rs +++ /dev/null @@ -1,247 +0,0 @@ -use std::ops::Deref; - -use itertools::Itertools; -use proc_macro2::TokenStream; -use quote::quote; -use syn::{DataEnum, Error}; - -use crate::{ - attrs::common::ContainerAttributes, - codegen::{ - chain::Neighbors, - common::{ - Container, ContainerInput, ContainerVersion, Item, ItemStatus, VersionedContainer, - }, - venum::variant::VersionedVariant, - }, -}; - -pub(crate) mod variant; - -/// Stores individual versions of a single enum. Each version tracks variant -/// actions, which describe if the variant was added, renamed or deprecated in -/// that version. Variants which are not versioned, are included in every -/// version of the enum. -#[derive(Debug)] -pub(crate) struct VersionedEnum(VersionedContainer); - -impl Deref for VersionedEnum { - type Target = VersionedContainer; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Container for VersionedEnum { - fn new( - input: ContainerInput, - data: DataEnum, - attributes: ContainerAttributes, - ) -> syn::Result { - let ident = &input.ident; - - // Convert the raw version attributes into a container version. - let versions: Vec<_> = (&attributes).into(); - - // Extract the attributes for every variant from the raw token - // stream and also validate that each variant action version uses a - // version declared by the container attribute. - let mut items = Vec::new(); - - for variant in data.variants { - let mut versioned_field = VersionedVariant::new(variant, &attributes)?; - versioned_field.insert_container_versions(&versions); - items.push(versioned_field); - } - - // Check for field ident collisions - for version in &versions { - // Collect the idents of all variants for a single version and then - // ensure that all idents are unique. If they are not, return an - // error. - - // TODO (@Techassi): Report which variant(s) use a duplicate ident and - // also hint what can be done to fix it based on the variant action / - // status. - - if !items.iter().map(|f| f.get_ident(version)).all_unique() { - return Err(Error::new( - ident.span(), - format!("Enum contains renamed variants which collide with other variants in version {version}", version = version.inner), - )); - } - } - - Ok(Self(VersionedContainer::new( - input, attributes, versions, items, - ))) - } - - 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 - } -} - -impl VersionedEnum { - fn generate_version( - &self, - version: &ContainerVersion, - next_version: Option<&ContainerVersion>, - ) -> TokenStream { - let mut token_stream = TokenStream::new(); - - let original_attributes = &self.original_attributes; - let enum_name = &self.idents.original; - let visibility = &self.visibility; - - // Generate variants of the enum for `version`. - let variants = self.generate_enum_variants(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 version_ident = &version.ident; - - let deprecated_note = format!("Version {version} is deprecated", version = version_ident); - let deprecated_attr = version - .deprecated - .then_some(quote! {#[deprecated = #deprecated_note]}); - - // Generate doc comments for the container (enum) - let version_specific_docs = self.generate_enum_docs(version); - - // Generate tokens for the module and the contained enum - token_stream.extend(quote! { - #[automatically_derived] - #deprecated_attr - #visibility mod #version_ident { - use super::*; - - #version_specific_docs - #(#original_attributes)* - pub enum #enum_name { - #variants - } - } - }); - - // Generate the From impl between this `version` and the next one. - if !self.options.skip_from && !version.skip_from { - token_stream.extend(self.generate_from_impl(version, next_version)); - } - - token_stream - } - - /// Generates version specific doc comments for the enum. - fn generate_enum_docs(&self, version: &ContainerVersion) -> TokenStream { - let mut tokens = TokenStream::new(); - - for (i, doc) in version.version_specific_docs.iter().enumerate() { - if i == 0 { - // Prepend an empty line to clearly separate the version - // specific docs. - tokens.extend(quote! { - #[doc = ""] - }) - } - tokens.extend(quote! { - #[doc = #doc] - }) - } - - tokens - } - - fn generate_enum_variants(&self, version: &ContainerVersion) -> TokenStream { - let mut token_stream = TokenStream::new(); - - for variant in &self.items { - token_stream.extend(variant.generate_for_container(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 module_name = &version.ident; - - let enum_ident = &self.idents.original; - let from_ident = &self.idents.from; - - let mut variants = TokenStream::new(); - - for item in &self.items { - variants.extend(item.generate_for_from_impl( - module_name, - next_module_name, - version, - next_version, - enum_ident, - )) - } - - // Include allow(deprecated) only when this or the next version is - // deprecated. Also include it, when a variant in this or the next - // version is deprecated. - let allow_attribute = (version.deprecated - || next_version.deprecated - || self.is_any_variant_deprecated(version) - || self.is_any_variant_deprecated(next_version)) - .then_some(quote! { #[allow(deprecated)] }); - - return quote! { - #[automatically_derived] - #allow_attribute - impl From<#module_name::#enum_ident> for #next_module_name::#enum_ident { - fn from(#from_ident: #module_name::#enum_ident) -> Self { - match #from_ident { - #variants - } - } - } - }; - } - - quote! {} - } - - /// Returns whether any field is deprecated in the provided - /// [`ContainerVersion`]. - fn is_any_variant_deprecated(&self, version: &ContainerVersion) -> bool { - // First, iterate over all fields. Any will return true if any of the - // function invocations return true. If a field doesn't have a chain, - // we can safely default to false (unversioned fields cannot be - // deprecated). Then we retrieve the status of the field and ensure it - // is deprecated. - self.items.iter().any(|f| { - f.chain.as_ref().map_or(false, |c| { - c.value_is(&version.inner, |a| { - matches!( - a, - ItemStatus::Deprecation { .. } - | ItemStatus::NoChange { - previously_deprecated: true, - .. - } - ) - }) - }) - }) - } -} diff --git a/crates/stackable-versioned-macros/src/codegen/venum/variant.rs b/crates/stackable-versioned-macros/src/codegen/venum/variant.rs deleted file mode 100644 index 5377e3ba2..000000000 --- a/crates/stackable-versioned-macros/src/codegen/venum/variant.rs +++ /dev/null @@ -1,246 +0,0 @@ -use std::ops::{Deref, DerefMut}; - -use darling::FromVariant; -use proc_macro2::{Span, TokenStream}; -use quote::{format_ident, quote}; -use syn::{token::Not, Ident, Type, TypeNever, Variant}; - -use crate::{ - attrs::{ - common::{ContainerAttributes, ItemAttributes}, - variant::VariantAttributes, - }, - codegen::{ - chain::BTreeMapExt, - common::{ - remove_deprecated_variant_prefix, Attributes, ContainerVersion, InnerItem, Item, - ItemStatus, Named, VersionedItem, - }, - }, -}; - -/// A versioned variant, which contains contains common [`Variant`] data and a -/// chain of actions. -/// -/// The chain of action maps versions to an action and the appropriate variant -/// name. Additionally, the [`Variant`] data can be used to forward attributes, -/// generate documentation, etc. -#[derive(Debug)] -pub(crate) struct VersionedVariant(VersionedItem); - -impl Deref for VersionedVariant { - type Target = VersionedItem; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for VersionedVariant { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl TryFrom<&Variant> for VariantAttributes { - type Error = darling::Error; - - fn try_from(variant: &Variant) -> Result { - Self::from_variant(variant) - } -} - -impl Attributes for VariantAttributes { - fn common_attributes_owned(self) -> ItemAttributes { - self.common - } - - fn common_attributes(&self) -> &ItemAttributes { - &self.common - } - - fn original_attributes(&self) -> &Vec { - &self.attrs - } -} - -impl InnerItem for Variant { - fn ty(&self) -> syn::Type { - // FIXME (@Techassi): As we currently don't support enum variants with - // data, we just return the Never type as the code generation code for - // enum variants won't use this type information. - Type::Never(TypeNever { - bang_token: Not([Span::call_site()]), - }) - } -} - -impl Named for Variant { - fn cleaned_ident(&self) -> Ident { - remove_deprecated_variant_prefix(self.ident()) - } - - fn ident(&self) -> &Ident { - &self.ident - } -} - -impl VersionedVariant { - /// Creates a new versioned variant. - /// - /// Internally this calls [`VersionedItem::new`] to handle most of the - /// common creation code. - pub(crate) fn new( - variant: Variant, - container_attributes: &ContainerAttributes, - ) -> syn::Result { - let item = VersionedItem::<_, VariantAttributes>::new(variant, container_attributes)?; - Ok(Self(item)) - } - - /// Generates tokens to be used in a container definition. - pub(crate) fn generate_for_container( - &self, - container_version: &ContainerVersion, - ) -> Option { - let original_attributes = &self.original_attributes; - let fields = &self.inner.fields; - - match &self.chain { - // NOTE (@Techassi): https://rust-lang.github.io/rust-clippy/master/index.html#/expect_fun_call - Some(chain) => match chain.get(&container_version.inner).unwrap_or_else(|| { - panic!( - "internal error: chain must contain container version {}", - container_version.inner - ) - }) { - ItemStatus::Addition { ident, .. } => Some(quote! { - #(#original_attributes)* - #ident #fields, - }), - ItemStatus::Change { to_ident, .. } => Some(quote! { - #(#original_attributes)* - #to_ident #fields, - }), - ItemStatus::Deprecation { ident, note, .. } => { - // FIXME (@Techassi): Emitting the deprecated attribute - // should cary over even when the item status is - // 'NoChange'. - // TODO (@Techassi): Make the generation of deprecated - // items customizable. When a container is used as a K8s - // CRD, the item must continue to exist, even when - // deprecated. For other versioning use-cases, that - // might not be the case. - let deprecated_attr = if let Some(note) = note { - quote! {#[deprecated = #note]} - } else { - quote! {#[deprecated]} - }; - - Some(quote! { - #(#original_attributes)* - #deprecated_attr - #ident #fields, - }) - } - ItemStatus::NoChange { - previously_deprecated, - ident, - .. - } => { - // TODO (@Techassi): Also carry along the deprecation - // note. - let deprecated_attr = previously_deprecated.then(|| quote! {#[deprecated]}); - - Some(quote! { - #(#original_attributes)* - #deprecated_attr - #ident #fields, - }) - } - ItemStatus::NotPresent => None, - }, - None => { - // If there is no chain of variant actions, the variant is not - // versioned and code generation is straight forward. - // Unversioned variants are always included in versioned enums. - let ident = &self.inner.ident; - - Some(quote! { - #(#original_attributes)* - #ident #fields, - }) - } - } - } - - /// Generates tokens to be used in a [`From`] implementation. - pub(crate) fn generate_for_from_impl( - &self, - module_name: &Ident, - next_module_name: &Ident, - version: &ContainerVersion, - next_version: &ContainerVersion, - enum_ident: &Ident, - ) -> TokenStream { - let variant_data = match &self.inner.fields { - syn::Fields::Named(fields_named) => { - let field_names = fields_named - .named - .iter() - .map(|field| { - field - .ident - .as_ref() - .expect("named fields always have an ident") - .clone() - }) - .collect::>(); - - let tokens = quote! { { #(#field_names),* } }; - tokens - } - syn::Fields::Unnamed(fields_unnamed) => { - let field_names = fields_unnamed - .unnamed - .iter() - .enumerate() - .map(|(index, _)| format_ident!("__sv_{index}")) - .collect::>(); - - let tokens = quote! { ( #(#field_names),* ) }; - tokens - } - - syn::Fields::Unit => TokenStream::new(), - }; - - match &self.chain { - Some(chain) => match ( - chain.get_expect(&version.inner), - chain.get_expect(&next_version.inner), - ) { - (_, ItemStatus::Addition { .. }) => quote! {}, - (old, next) => { - let old_variant_ident = old - .get_ident() - .expect("internal error: old variant must have a name"); - let next_variant_ident = next - .get_ident() - .expect("internal error: next variant must have a name"); - - quote! { - #module_name::#enum_ident::#old_variant_ident #variant_data => #next_module_name::#enum_ident::#next_variant_ident #variant_data, - } - } - }, - None => { - let variant_ident = &self.inner.ident; - - quote! { - #module_name::#enum_ident::#variant_ident #variant_data => #next_module_name::#enum_ident::#variant_ident #variant_data, - } - } - } - } -} diff --git a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs b/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs deleted file mode 100644 index 6de44835f..000000000 --- a/crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs +++ /dev/null @@ -1,445 +0,0 @@ -use std::ops::Deref; - -use itertools::Itertools; -use proc_macro2::TokenStream; -use quote::quote; -use syn::{parse_quote, DataStruct, Error, Ident}; - -use crate::{ - attrs::common::ContainerAttributes, - codegen::{ - chain::Neighbors, - common::{ - Container, ContainerInput, ContainerVersion, Item, ItemStatus, VersionExt, - VersionedContainer, - }, - vstruct::field::VersionedField, - }, -}; - -pub(crate) mod field; - -type GenerateVersionReturn = (TokenStream, Option<(TokenStream, (Ident, String))>); - -/// 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(VersionedContainer); - -impl Deref for VersionedStruct { - type Target = VersionedContainer; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Container for VersionedStruct { - fn new( - input: ContainerInput, - data: DataStruct, - attributes: ContainerAttributes, - ) -> syn::Result { - let ident = &input.ident; - - // Convert the raw version attributes into a container version. - let versions: Vec<_> = (&attributes).into(); - - // 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 items = Vec::new(); - - for field in data.fields { - let mut versioned_field = VersionedField::new(field, &attributes)?; - versioned_field.insert_container_versions(&versions); - items.push(versioned_field); - } - - // Check for field ident collisions - for version in &versions { - // Collect the idents of all fields for a single version and then - // ensure that all idents are unique. If they are not, return an - // error. - - // TODO (@Techassi): Report which field(s) use a duplicate ident and - // also hint what can be done to fix it based on the field action / - // status. - - if !items.iter().map(|f| f.get_ident(version)).all_unique() { - return Err(Error::new( - ident.span(), - format!("struct contains renamed fields which collide with other fields in version {version}", version = version.inner), - )); - } - } - - // Validate K8s specific requirements - // Ensure that the struct name includes the 'Spec' suffix. - if attributes.kubernetes_attrs.is_some() && !ident.to_string().ends_with("Spec") { - return Err(Error::new( - ident.span(), - "struct name needs to include the `Spec` suffix if Kubernetes features are enabled via `#[versioned(k8s())]`" - )); - } - - Ok(Self(VersionedContainer::new( - input, attributes, versions, items, - ))) - } - - fn generate_tokens(&self) -> TokenStream { - let mut tokens = TokenStream::new(); - - let mut enum_variants = Vec::new(); - let mut crd_fn_calls = Vec::new(); - - let mut versions = self.versions.iter().peekable(); - - while let Some(version) = versions.next() { - let (container_definition, merged_crd) = - self.generate_version(version, versions.peek().copied()); - - if let Some((crd_fn_call, enum_variant)) = merged_crd { - enum_variants.push(enum_variant); - crd_fn_calls.push(crd_fn_call); - } - - tokens.extend(container_definition); - } - - if !crd_fn_calls.is_empty() { - tokens.extend(self.generate_kubernetes_merge_crds(crd_fn_calls, enum_variants)); - } - - tokens - } -} - -impl VersionedStruct { - /// Generates all tokens for a single instance of a versioned struct. - fn generate_version( - &self, - version: &ContainerVersion, - next_version: Option<&ContainerVersion>, - ) -> GenerateVersionReturn { - let mut token_stream = TokenStream::new(); - - let original_attributes = &self.original_attributes; - let struct_name = &self.idents.original; - let visibility = &self.visibility; - - // 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 version_ident = &version.ident; - - let deprecated_note = format!("Version {version} is deprecated", version = version_ident); - let deprecated_attr = version - .deprecated - .then_some(quote! {#[deprecated = #deprecated_note]}); - - // Generate doc comments for the container (struct) - let version_specific_docs = self.generate_struct_docs(version); - - // Generate K8s specific code - let (kubernetes_cr_derive, merged_crd) = match &self.options.kubernetes_options { - Some(options) => { - // Generate the CustomResource derive macro with the appropriate - // attributes supplied using #[kube()]. - let cr_derive = self.generate_kubernetes_cr_derive(version); - - // Generate merged_crd specific code when not opted out. - let merged_crd = if !options.skip_merged_crd { - let crd_fn_call = self.generate_kubernetes_crd_fn_call(version); - let enum_variant = version.inner.as_variant_ident(); - let enum_display = version.inner.to_string(); - - Some((crd_fn_call, (enum_variant, enum_display))) - } else { - None - }; - - (Some(cr_derive), merged_crd) - } - None => (None, None), - }; - - // Generate tokens for the module and the contained struct - token_stream.extend(quote! { - #[automatically_derived] - #deprecated_attr - #visibility mod #version_ident { - use super::*; - - #version_specific_docs - #(#original_attributes)* - #kubernetes_cr_derive - pub struct #struct_name { - #fields - } - } - }); - - // Generate the From impl between this `version` and the next one. - if !self.options.skip_from && !version.skip_from { - token_stream.extend(self.generate_from_impl(version, next_version)); - } - - (token_stream, merged_crd) - } - - /// Generates version specific doc comments for the struct. - fn generate_struct_docs(&self, version: &ContainerVersion) -> TokenStream { - let mut tokens = TokenStream::new(); - - for (i, doc) in version.version_specific_docs.iter().enumerate() { - if i == 0 { - // Prepend an empty line to clearly separate the version - // specific docs. - tokens.extend(quote! { - #[doc = ""] - }) - } - tokens.extend(quote! { - #[doc = #doc] - }) - } - - tokens - } - - /// Generates struct fields following the `name: type` format which includes - /// a trailing comma. - fn generate_struct_fields(&self, version: &ContainerVersion) -> TokenStream { - let mut tokens = TokenStream::new(); - - for item in &self.items { - tokens.extend(item.generate_for_container(version)); - } - - tokens - } - - /// Generates the [`From`] impl which enables conversion between a version - /// and the next one. - fn generate_from_impl( - &self, - version: &ContainerVersion, - next_version: Option<&ContainerVersion>, - ) -> Option { - if let Some(next_version) = next_version { - let next_module_name = &next_version.ident; - let module_name = &version.ident; - - let struct_ident = &self.idents.original; - let from_ident = &self.idents.from; - - let fields = self.generate_from_fields(version, next_version, from_ident); - - // Include allow(deprecated) only when this or the next version is - // deprecated. Also include it, when a field in this or the next - // version is deprecated. - let allow_attribute = (version.deprecated - || next_version.deprecated - || self.is_any_field_deprecated(version) - || self.is_any_field_deprecated(next_version)) - .then_some(quote! { #[allow(deprecated)] }); - - return Some(quote! { - #[automatically_derived] - #allow_attribute - impl From<#module_name::#struct_ident> for #next_module_name::#struct_ident { - fn from(#from_ident: #module_name::#struct_ident) -> Self { - Self { - #fields - } - } - } - }); - } - - None - } - - /// Generates fields used in the [`From`] impl following the - /// `new_name: struct_name.old_name` format which includes a trailing comma. - fn generate_from_fields( - &self, - version: &ContainerVersion, - next_version: &ContainerVersion, - from_ident: &Ident, - ) -> TokenStream { - let mut token_stream = TokenStream::new(); - - for item in &self.items { - token_stream.extend(item.generate_for_from_impl(version, next_version, from_ident)) - } - - token_stream - } - - /// Returns whether any field is deprecated in the provided - /// [`ContainerVersion`]. - fn is_any_field_deprecated(&self, version: &ContainerVersion) -> bool { - // First, iterate over all fields. Any will return true if any of the - // function invocations return true. If a field doesn't have a chain, - // we can safely default to false (unversioned fields cannot be - // deprecated). Then we retrieve the status of the field and ensure it - // is deprecated. - self.items.iter().any(|f| { - f.chain.as_ref().map_or(false, |c| { - c.value_is(&version.inner, |a| { - matches!( - a, - ItemStatus::Deprecation { .. } - | ItemStatus::NoChange { - previously_deprecated: true, - .. - } - ) - }) - }) - }) - } -} - -// Kubernetes specific code generation -impl VersionedStruct { - /// Generates the `kube::CustomResource` derive with the appropriate macro - /// attributes. - fn generate_kubernetes_cr_derive(&self, version: &ContainerVersion) -> Option { - if let Some(kubernetes_options) = &self.options.kubernetes_options { - // Required arguments - let group = &kubernetes_options.group; - let version = version.inner.to_string(); - let kind = kubernetes_options - .kind - .as_ref() - .map_or(self.idents.kubernetes.to_string(), |kind| kind.clone()); - - // Optional arguments - let namespaced = kubernetes_options - .namespaced - .then_some(quote! { , namespaced }); - let singular = kubernetes_options - .singular - .as_ref() - .map(|s| quote! { , singular = #s }); - let plural = kubernetes_options - .plural - .as_ref() - .map(|p| quote! { , plural = #p }); - - return Some(quote! { - #[derive(::kube::CustomResource)] - #[kube(group = #group, version = #version, kind = #kind #singular #plural #namespaced)] - }); - } - - None - } - - /// Generates the `merge_crds` function call. - fn generate_kubernetes_merge_crds( - &self, - crd_fn_calls: Vec, - enum_variants: Vec<(Ident, String)>, - ) -> TokenStream { - let enum_ident = &self.idents.kubernetes; - let enum_vis = &self.visibility; - - let mut enum_display_impl_matches = TokenStream::new(); - let mut enum_variant_idents = TokenStream::new(); - - for (enum_variant_ident, enum_variant_display) in enum_variants { - enum_variant_idents.extend(quote! {#enum_variant_ident,}); - enum_display_impl_matches.extend(quote! { - #enum_ident::#enum_variant_ident => f.write_str(#enum_variant_display), - }); - } - - quote! { - #[automatically_derived] - #enum_vis enum #enum_ident { - #enum_variant_idents - } - - #[automatically_derived] - impl ::std::fmt::Display for #enum_ident { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { - match self { - #enum_display_impl_matches - } - } - } - - #[automatically_derived] - impl #enum_ident { - /// Generates a merged CRD which contains all versions defined using the `#[versioned()]` macro. - pub fn merged_crd( - stored_apiversion: Self - ) -> ::std::result::Result<::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError> { - ::kube::core::crd::merge_crds(vec![#(#crd_fn_calls),*], &stored_apiversion.to_string()) - } - - /// Generates and writes a merged CRD which contains all versions defined using the `#[versioned()]` - /// macro to a file located at `path`. - pub fn write_merged_crd

(path: P, stored_apiversion: Self, operator_version: &str) -> Result<(), ::stackable_versioned::Error> - where P: AsRef<::std::path::Path> - { - use ::stackable_shared::yaml::{YamlSchema, SerializeOptions}; - - let merged_crd = Self::merged_crd(stored_apiversion).map_err(|err| ::stackable_versioned::Error::MergeCrd { - source: err, - })?; - - YamlSchema::write_yaml_schema( - &merged_crd, - path, - operator_version, - SerializeOptions::default() - ).map_err(|err| ::stackable_versioned::Error::SerializeYaml { - source: err, - }) - } - - /// Generates and writes a merged CRD which contains all versions defined using the `#[versioned()]` - /// macro to stdout. - pub fn print_merged_crd(stored_apiversion: Self, operator_version: &str) -> Result<(), ::stackable_versioned::Error> { - use ::stackable_shared::yaml::{YamlSchema, SerializeOptions}; - - let merged_crd = Self::merged_crd(stored_apiversion).map_err(|err| ::stackable_versioned::Error::MergeCrd { - source: err, - })?; - - YamlSchema::print_yaml_schema( - &merged_crd, - operator_version, - SerializeOptions::default() - ).map_err(|err| ::stackable_versioned::Error::SerializeYaml { - source: err, - }) - } - } - } - } - - /// Generates the inner `crd()` functions calls which get used in the - /// `merge_crds` function. - fn generate_kubernetes_crd_fn_call(&self, version: &ContainerVersion) -> TokenStream { - let struct_ident = &self.idents.kubernetes; - let version_ident = &version.ident; - let path: syn::Path = parse_quote!(#version_ident::#struct_ident); - - quote! { - <#path as ::kube::CustomResourceExt>::crd() - } - } -} diff --git a/crates/stackable-versioned-macros/src/consts.rs b/crates/stackable-versioned-macros/src/consts.rs deleted file mode 100644 index d064c344d..000000000 --- a/crates/stackable-versioned-macros/src/consts.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) const DEPRECATED_VARIANT_PREFIX: &str = "Deprecated"; -pub(crate) const DEPRECATED_FIELD_PREFIX: &str = "deprecated_"; diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs index 469adae82..8e2750d09 100644 --- a/crates/stackable-versioned-macros/src/lib.rs +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -1,15 +1,22 @@ use darling::{ast::NestedMeta, FromMeta}; use proc_macro::TokenStream; -use syn::{DeriveInput, Error}; +use syn::{spanned::Spanned, Error, Item}; -use crate::attrs::common::ContainerAttributes; +use crate::{ + attrs::{container::StandaloneContainerAttributes, module::ModuleAttributes}, + codegen::{ + container::{Container, StandaloneContainer}, + module::{Module, ModuleInput}, + VersionDefinition, + }, +}; #[cfg(test)] mod test_utils; mod attrs; mod codegen; -mod consts; +mod utils; /// This macro enables generating versioned structs and enums. /// @@ -476,26 +483,103 @@ println!("{}", serde_yaml::to_string(&merged_crd).unwrap()); /// a cluster scoped. #[proc_macro_attribute] pub fn versioned(attrs: TokenStream, input: TokenStream) -> TokenStream { - // NOTE (@Techassi): For now, we can just use the DeriveInput type here, - // because we only support structs end 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); + let input = syn::parse_macro_input!(input as Item); versioned_impl(attrs.into(), input).into() } -fn versioned_impl(attrs: proc_macro2::TokenStream, input: DeriveInput) -> proc_macro2::TokenStream { - let attrs = match NestedMeta::parse_meta_list(attrs) { - Ok(attrs) => match ContainerAttributes::from_list(&attrs) { - Ok(attrs) => attrs, - Err(err) => return err.write_errors(), - }, - Err(err) => return darling::Error::from(err).write_errors(), - }; +fn versioned_impl(attrs: proc_macro2::TokenStream, input: Item) -> proc_macro2::TokenStream { + // TODO (@Techassi): Think about how we can handle nested structs / enums which + // are also versioned. - codegen::expand(attrs, input).unwrap_or_else(Error::into_compile_error) + match input { + Item::Mod(item_mod) => { + let module_attributes: ModuleAttributes = match parse_outer_attributes(attrs) { + Ok(ma) => ma, + Err(err) => return err.write_errors(), + }; + + let versions: Vec = (&module_attributes).into(); + let preserve_modules = module_attributes.preserve_module.is_present(); + + let module_span = item_mod.span(); + let module_input = ModuleInput { + ident: item_mod.ident, + vis: item_mod.vis, + }; + + let Some((_, items)) = item_mod.content else { + return Error::new(module_span, "the macro can only be used on module blocks") + .into_compile_error(); + }; + + let mut containers = Vec::new(); + + for item in items { + let container = match item { + Item::Enum(item_enum) => { + match Container::new_enum_nested(item_enum, &versions) { + Ok(container) => container, + Err(err) => return err.write_errors(), + } + } + Item::Struct(item_struct) => { + match Container::new_struct_nested(item_struct, &versions) { + Ok(container) => container, + Err(err) => return err.write_errors(), + } + } + _ => continue, + }; + + containers.push(container); + } + + Module::new(module_input, preserve_modules, versions, containers).generate_tokens() + } + Item::Enum(item_enum) => { + let container_attributes: StandaloneContainerAttributes = + match parse_outer_attributes(attrs) { + Ok(ca) => ca, + Err(err) => return err.write_errors(), + }; + + let standalone_enum = + match StandaloneContainer::new_enum(item_enum, container_attributes) { + Ok(standalone_enum) => standalone_enum, + Err(err) => return err.write_errors(), + }; + + standalone_enum.generate_tokens() + } + Item::Struct(item_struct) => { + let container_attributes: StandaloneContainerAttributes = + match parse_outer_attributes(attrs) { + Ok(ca) => ca, + Err(err) => return err.write_errors(), + }; + + let standalone_struct = + match StandaloneContainer::new_struct(item_struct, container_attributes) { + Ok(standalone_struct) => standalone_struct, + Err(err) => return err.write_errors(), + }; + + standalone_struct.generate_tokens() + } + _ => Error::new( + input.span(), + "attribute macro `versioned` can be only be applied to modules, structs and enums", + ) + .into_compile_error(), + } +} + +fn parse_outer_attributes(attrs: proc_macro2::TokenStream) -> Result +where + T: FromMeta, +{ + let nm = NestedMeta::parse_meta_list(attrs)?; + T::from_list(&nm) } #[cfg(test)] diff --git a/crates/stackable-versioned-macros/src/test_utils.rs b/crates/stackable-versioned-macros/src/test_utils.rs index 016fa9433..cc4960382 100644 --- a/crates/stackable-versioned-macros/src/test_utils.rs +++ b/crates/stackable-versioned-macros/src/test_utils.rs @@ -8,7 +8,7 @@ use insta::Settings; use proc_macro2::TokenStream; use regex::Regex; use snafu::{OptionExt, ResultExt, Snafu}; -use syn::DeriveInput; +use syn::Item; use crate::versioned_impl; @@ -50,7 +50,7 @@ pub(crate) fn expand_from_file(path: &Path) -> Result { Ok(prettyplease::unparse(&parsed)) } -fn prepare_from_string(input: String) -> Result<(TokenStream, DeriveInput), Error> { +fn prepare_from_string(input: String) -> Result<(TokenStream, Item), Error> { let (attrs, input) = input.split_once(DELIMITER).context(MissingDelimiterSnafu)?; let attrs = REGEX diff --git a/crates/stackable-versioned-macros/src/utils.rs b/crates/stackable-versioned-macros/src/utils.rs new file mode 100644 index 000000000..d2003c7b2 --- /dev/null +++ b/crates/stackable-versioned-macros/src/utils.rs @@ -0,0 +1,117 @@ +use std::ops::Deref; + +use convert_case::{Case, Casing}; +use darling::util::IdentString; +use k8s_version::Version; +use quote::{format_ident, ToTokens}; +use syn::{spanned::Spanned, Ident}; + +pub(crate) trait VersionExt { + fn as_variant_ident(&self) -> IdentString; +} + +impl VersionExt for Version { + fn as_variant_ident(&self) -> IdentString { + format_ident!("{ident}", ident = self.to_string().to_case(Case::Pascal)).into() + } +} + +/// Provides extra functionality on top of [`IdentString`]s used to name containers. +pub(crate) trait ContainerIdentExt { + /// Removes the 'Spec' suffix from the [`IdentString`]. + fn as_cleaned_kubernetes_ident(&self) -> IdentString; + + /// Transforms the [`IdentString`] into one usable in the [`From`] impl. + fn as_from_impl_ident(&self) -> IdentString; +} + +impl ContainerIdentExt for Ident { + fn as_cleaned_kubernetes_ident(&self) -> IdentString { + let ident = format_ident!("{}", self.to_string().trim_end_matches("Spec")); + IdentString::new(ident) + } + + fn as_from_impl_ident(&self) -> IdentString { + let ident = format_ident!("__sv_{}", self.to_string().to_lowercase()); + IdentString::new(ident) + } +} + +pub(crate) trait ItemIdentExt: Deref + From + Spanned { + const DEPRECATED_PREFIX: &'static str; + + fn deprecated_prefix(&self) -> &'static str { + Self::DEPRECATED_PREFIX + } + + fn starts_with_deprecated_prefix(&self) -> bool { + self.deref().as_str().starts_with(Self::DEPRECATED_PREFIX) + } + + /// Removes deprecation prefixed from field or variant idents. + fn as_cleaned_ident(&self) -> IdentString; +} + +pub(crate) struct FieldIdent(IdentString); + +impl ItemIdentExt for FieldIdent { + const DEPRECATED_PREFIX: &'static str = "deprecated_"; + + fn as_cleaned_ident(&self) -> IdentString { + self.0 + .clone() + .map(|i| i.trim_start_matches(Self::DEPRECATED_PREFIX).to_string()) + } +} + +impl From for FieldIdent { + fn from(value: Ident) -> Self { + Self(IdentString::from(value)) + } +} + +impl Deref for FieldIdent { + type Target = IdentString; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ToTokens for FieldIdent { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.0.to_tokens(tokens); + } +} + +pub(crate) struct VariantIdent(IdentString); + +impl ItemIdentExt for VariantIdent { + const DEPRECATED_PREFIX: &'static str = "Deprecated"; + + fn as_cleaned_ident(&self) -> IdentString { + self.0 + .clone() + .map(|i| i.trim_start_matches(Self::DEPRECATED_PREFIX).to_string()) + } +} + +impl From for VariantIdent { + fn from(value: Ident) -> Self { + Self(IdentString::from(value)) + } +} + +impl Deref for VariantIdent { + type Target = IdentString; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ToTokens for VariantIdent { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.0.to_tokens(tokens); + } +} diff --git a/crates/stackable-versioned-macros/tests/default/fail/changed.stderr b/crates/stackable-versioned-macros/tests/default/fail/changed.stderr index a6d0d4070..9f0b1519e 100644 --- a/crates/stackable-versioned-macros/tests/default/fail/changed.stderr +++ b/crates/stackable-versioned-macros/tests/default/fail/changed.stderr @@ -1,10 +1,10 @@ -error: the previous field name must not start with the deprecation prefix +error: the previous name must not start with the deprecation prefix --> tests/default/fail/changed.rs:11:52 | 11 | changed(since = "v1beta1", from_name = "deprecated_bar"), | ^^^^^^^^^^^^^^^^ -error: the previous field name must not start with the deprecation prefix +error: the previous name must not start with the deprecation prefix --> tests/default/fail/changed.rs:12:47 | 12 | changed(since = "v1", from_name = "deprecated_baz") diff --git a/crates/stackable-versioned-macros/tests/default/fail/deprecate.stderr b/crates/stackable-versioned-macros/tests/default/fail/deprecate.stderr index c18cca62d..271b0e082 100644 --- a/crates/stackable-versioned-macros/tests/default/fail/deprecate.stderr +++ b/crates/stackable-versioned-macros/tests/default/fail/deprecate.stderr @@ -1,4 +1,4 @@ -error: deprecation must be done using #[versioned(deprecated(since = "VERSION"))] +error: deprecation must be done using `#[versioned(deprecated(since = "VERSION"))]` --> tests/default/fail/deprecate.rs:10:9 | 10 | #[deprecated] diff --git a/crates/stackable-versioned/CHANGELOG.md b/crates/stackable-versioned/CHANGELOG.md index 8274d650b..280a7d795 100644 --- a/crates/stackable-versioned/CHANGELOG.md +++ b/crates/stackable-versioned/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Add support to apply the `#[versioned()]` macro to modules to version all contained items at + once ([#891]). + +### Changed + +- Bump Rust to 1.82.0 ([#891]). + +[#891]: https://github.com/stackabletech/operator-rs/pull/891 + ## [0.4.1] - 2024-10-23 ### Added diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 1de01fa45..2e2b8c852 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.81.0" +channel = "1.82.0"