diff --git a/crates/stackable-versioned-macros/src/attrs/module.rs b/crates/stackable-versioned-macros/src/attrs/module.rs index 71d61d34b..5c5983bd5 100644 --- a/crates/stackable-versioned-macros/src/attrs/module.rs +++ b/crates/stackable-versioned-macros/src/attrs/module.rs @@ -95,7 +95,11 @@ pub struct ModuleSkipArguments { /// This struct contains crate overrides to be passed to `#[kube]`. #[derive(Clone, Debug, FromMeta)] +#[darling(and_then = "CrateArguments::dynamic_default")] pub struct CrateArguments { + #[darling(default = default_kube)] + pub kube: Override, + #[darling(default = default_kube_core)] pub kube_core: Override, @@ -124,6 +128,7 @@ pub struct CrateArguments { impl Default for CrateArguments { fn default() -> Self { Self { + kube: default_kube(), kube_core: default_kube_core(), kube_client: default_kube_client(), k8s_openapi: default_k8s_openapi(), @@ -136,6 +141,30 @@ impl Default for CrateArguments { } } +impl CrateArguments { + fn dynamic_default(mut self) -> Result { + // Adjust the kube_core and kube_client paths automatically if only the kube path is + // overridden. + if let Override::Explicit(kube_path) = &self.kube { + if self.kube_core.is_explicit() || self.kube_client.is_explicit() { + return Err( + Error::custom("the `kube` crate override is mutually exclusive with `kube_core` and `kube_client`") + .with_span(&kube_path) + ); + } + + self.kube_core = Override::Explicit(parse_quote! { #kube_path::core }); + self.kube_client = Override::Explicit(parse_quote! { #kube_path::client }); + } + + Ok(self) + } +} + +fn default_kube() -> Override { + Override::Default(parse_quote! { ::kube }) +} + fn default_kube_core() -> Override { Override::Default(parse_quote! { ::kube::core }) } @@ -262,3 +291,12 @@ impl Deref for Override { } } } + +impl Override { + pub fn is_explicit(&self) -> bool { + match self { + Override::Default(_) => false, + Override::Explicit(_) => true, + } + } +} diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs index 1266615e5..83997e750 100644 --- a/crates/stackable-versioned-macros/src/lib.rs +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -200,7 +200,7 @@ mod utils; /// the crates are brought into scope through re-exports. The following code /// block depicts supported overrides and their default values. /// -/// ``` +/// ```ignore /// # use stackable_versioned_macros::versioned; /// #[versioned( /// version(name = "v1alpha1"), @@ -213,6 +213,8 @@ mod utils; /// kube_core = "::kube::core", /// schemars = "::schemars", /// serde = "::serde", +/// // Mutually exclusive with kube_core and kube_client +/// kube = "::kube", /// ) /// )] /// mod versioned { diff --git a/crates/stackable-versioned-macros/tests/inputs/pass/crate_overrides_only_kube.rs b/crates/stackable-versioned-macros/tests/inputs/pass/crate_overrides_only_kube.rs new file mode 100644 index 000000000..5bcfa10c9 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/pass/crate_overrides_only_kube.rs @@ -0,0 +1,30 @@ +use stackable_versioned::versioned; +// --- +#[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + version(name = "v1"), + crates( + kube = ::kube, + schemars = ::schemars + ) +)] +// --- +pub mod versioned { + #[versioned(crd(group = "foo.example.org"))] + #[derive( + Clone, + Debug, + serde::Deserialize, + serde::Serialize, + schemars::JsonSchema, + kube::CustomResource, + )] + pub struct FooSpec { + #[versioned(added(since = "v1beta1"), changed(since = "v1", from_name = "bah"))] + bar: usize, + baz: bool, + } +} +// --- +fn main() {} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@crate_overrides_only_kube.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@crate_overrides_only_kube.rs.snap new file mode 100644 index 000000000..35a1815f4 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@crate_overrides_only_kube.rs.snap @@ -0,0 +1,423 @@ +--- +source: crates/stackable-versioned-macros/src/lib.rs +expression: formatted +input_file: crates/stackable-versioned-macros/tests/inputs/pass/crate_overrides_only_kube.rs +--- +#[automatically_derived] +pub mod v1alpha1 { + use super::*; + #[derive( + Clone, + Debug, + serde::Deserialize, + serde::Serialize, + schemars::JsonSchema, + kube::CustomResource, + )] + #[kube( + group = "foo.example.org", + version = "v1alpha1", + kind = "Foo", + crates(kube_core = ::kube::core, schemars = ::schemars) + )] + pub struct FooSpec { + pub baz: bool, + } +} +#[automatically_derived] +impl ::core::convert::From for v1beta1::Foo { + fn from(__sv_foo: v1alpha1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1alpha1::Foo { + fn from(__sv_foo: v1beta1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1beta1::FooSpec { + fn from(__sv_foospec: v1alpha1::FooSpec) -> Self { + Self { + bah: ::std::default::Default::default(), + baz: __sv_foospec.baz.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1alpha1::FooSpec { + fn from(__sv_foospec: v1beta1::FooSpec) -> Self { + Self { + baz: __sv_foospec.baz.into(), + } + } +} +#[automatically_derived] +pub mod v1beta1 { + use super::*; + #[derive( + Clone, + Debug, + serde::Deserialize, + serde::Serialize, + schemars::JsonSchema, + kube::CustomResource, + )] + #[kube( + group = "foo.example.org", + version = "v1beta1", + kind = "Foo", + crates(kube_core = ::kube::core, schemars = ::schemars) + )] + pub struct FooSpec { + pub bah: usize, + pub baz: bool, + } +} +#[automatically_derived] +impl ::core::convert::From for v1::Foo { + fn from(__sv_foo: v1beta1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1beta1::Foo { + fn from(__sv_foo: v1::Foo) -> Self { + Self { + metadata: __sv_foo.metadata, + spec: __sv_foo.spec.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1::FooSpec { + fn from(__sv_foospec: v1beta1::FooSpec) -> Self { + Self { + bar: __sv_foospec.bah.into(), + baz: __sv_foospec.baz.into(), + } + } +} +#[automatically_derived] +impl ::core::convert::From for v1beta1::FooSpec { + fn from(__sv_foospec: v1::FooSpec) -> Self { + Self { + bah: __sv_foospec.bar.into(), + baz: __sv_foospec.baz.into(), + } + } +} +#[automatically_derived] +pub mod v1 { + use super::*; + #[derive( + Clone, + Debug, + serde::Deserialize, + serde::Serialize, + schemars::JsonSchema, + kube::CustomResource, + )] + #[kube( + group = "foo.example.org", + version = "v1", + kind = "Foo", + crates(kube_core = ::kube::core, schemars = ::schemars) + )] + pub struct FooSpec { + pub bar: usize, + pub baz: bool, + } +} +#[automatically_derived] +#[derive(::core::fmt::Debug)] +pub enum Foo { + V1Alpha1(v1alpha1::Foo), + V1Beta1(v1beta1::Foo), + V1(v1::Foo), +} +#[automatically_derived] +impl Foo { + /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. + pub fn merged_crd( + stored_apiversion: FooVersion, + ) -> ::std::result::Result< + ::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, + ::kube::core::crd::MergeError, + > { + ::kube::core::crd::merge_crds( + vec![ + < v1alpha1::Foo as ::kube::core::CustomResourceExt > ::crd(), < + v1beta1::Foo as ::kube::core::CustomResourceExt > ::crd(), < v1::Foo as + ::kube::core::CustomResourceExt > ::crd() + ], + stored_apiversion.as_version_str(), + ) + } + ///Tries to convert a list of objects of kind [`Foo`] to the desired API version + ///specified in the [`ConversionReview`][cr]. + /// + ///The returned [`ConversionReview`][cr] either indicates a success or a failure, which + ///is handed back to the Kubernetes API server. + /// + ///[cr]: ::kube::core::conversion::ConversionReview + pub fn try_convert( + review: ::kube::core::conversion::ConversionReview, + ) -> ::kube::core::conversion::ConversionReview { + let request = match ::kube::core::conversion::ConversionRequest::from_review( + review, + ) { + ::std::result::Result::Ok(request) => request, + ::std::result::Result::Err(err) => { + return ::kube::core::conversion::ConversionResponse::invalid(::kube::core::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: err.to_string(), + metadata: None, + reason: err.to_string(), + details: None, + code: 400, + }) + .into_review(); + } + }; + let response = match Self::convert_objects( + request.objects, + &request.desired_api_version, + ) { + ::std::result::Result::Ok(converted_objects) => { + ::kube::core::conversion::ConversionResponse { + result: ::kube::core::Status::success(), + types: request.types, + uid: request.uid, + converted_objects, + } + } + ::std::result::Result::Err(err) => { + let code = err.http_status_code(); + let message = err.join_errors(); + ::kube::core::conversion::ConversionResponse { + result: ::kube::core::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: message.clone(), + metadata: None, + reason: message, + details: None, + code, + }, + types: request.types, + uid: request.uid, + converted_objects: vec![], + } + } + }; + response.into_review() + } + fn convert_objects( + objects: ::std::vec::Vec<::serde_json::Value>, + desired_api_version: &str, + ) -> ::std::result::Result< + ::std::vec::Vec<::serde_json::Value>, + ::stackable_versioned::ConvertObjectError, + > { + let desired_api_version = FooVersion::from_api_version(desired_api_version) + .map_err(|source| ::stackable_versioned::ConvertObjectError::ParseDesiredApiVersion { + source, + })?; + let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); + for object in objects { + let current_object = Self::from_json_object(object.clone()) + .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { + source, + })?; + match (current_object, desired_api_version) { + (Self::V1Alpha1(__sv_foo), FooVersion::V1Beta1) => { + let converted: v1beta1::Foo = __sv_foo.into(); + let desired_object = Self::V1Beta1(converted); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Alpha1(__sv_foo), FooVersion::V1) => { + let converted: v1beta1::Foo = __sv_foo.into(); + let converted: v1::Foo = converted.into(); + let desired_object = Self::V1(converted); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Beta1(__sv_foo), FooVersion::V1Alpha1) => { + let converted: v1alpha1::Foo = __sv_foo.into(); + let desired_object = Self::V1Alpha1(converted); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Beta1(__sv_foo), FooVersion::V1) => { + let converted: v1::Foo = __sv_foo.into(); + let desired_object = Self::V1(converted); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foo), FooVersion::V1Alpha1) => { + let converted: v1beta1::Foo = __sv_foo.into(); + let converted: v1alpha1::Foo = converted.into(); + let desired_object = Self::V1Alpha1(converted); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foo), FooVersion::V1Beta1) => { + let converted: v1beta1::Foo = __sv_foo.into(); + let desired_object = Self::V1Beta1(converted); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + _ => converted_objects.push(object), + } + } + ::std::result::Result::Ok(converted_objects) + } + fn from_json_object( + object_value: ::serde_json::Value, + ) -> ::std::result::Result { + let kind = object_value + .get("kind") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { + field: "kind".to_owned(), + })? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "kind".to_owned(), + })?; + if kind != "Foo" { + return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { + kind: kind.to_owned(), + expected: "Foo".to_owned(), + }); + } + let api_version = object_value + .get("apiVersion") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { + field: "apiVersion".to_owned(), + })? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "apiVersion".to_owned(), + })?; + let object = match api_version { + "foo.example.org/v1alpha1" => { + let object = ::serde_json::from_value(object_value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Alpha1(object) + } + "foo.example.org/v1beta1" => { + let object = ::serde_json::from_value(object_value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Beta1(object) + } + "foo.example.org/v1" => { + let object = ::serde_json::from_value(object_value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1(object) + } + unknown_api_version => { + return ::std::result::Result::Err(::stackable_versioned::ParseObjectError::UnknownApiVersion { + api_version: unknown_api_version.to_owned(), + }); + } + }; + ::std::result::Result::Ok(object) + } + fn into_json_value( + self, + ) -> ::std::result::Result<::serde_json::Value, ::serde_json::Error> { + match self { + Self::V1Alpha1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + Self::V1Beta1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + Self::V1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + } + } +} +#[automatically_derived] +#[derive(::core::marker::Copy, ::core::clone::Clone, ::core::fmt::Debug)] +pub enum FooVersion { + V1Alpha1, + V1Beta1, + V1, +} +#[automatically_derived] +impl ::core::fmt::Display for FooVersion { + fn fmt( + &self, + f: &mut ::core::fmt::Formatter<'_>, + ) -> ::std::result::Result<(), ::std::fmt::Error> { + f.write_str(self.as_version_str()) + } +} +#[automatically_derived] +impl FooVersion { + pub fn as_version_str(&self) -> &str { + match self { + FooVersion::V1Alpha1 => "v1alpha1", + FooVersion::V1Beta1 => "v1beta1", + FooVersion::V1 => "v1", + } + } + pub fn as_api_version_str(&self) -> &str { + match self { + FooVersion::V1Alpha1 => "foo.example.org/v1alpha1", + FooVersion::V1Beta1 => "foo.example.org/v1beta1", + FooVersion::V1 => "foo.example.org/v1", + } + } + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "foo.example.org/v1alpha1" => Ok(FooVersion::V1Alpha1), + "foo.example.org/v1beta1" => Ok(FooVersion::V1Beta1), + "foo.example.org/v1" => Ok(FooVersion::V1), + _ => { + Err(::stackable_versioned::UnknownDesiredApiVersionError { + api_version: api_version.to_owned(), + }) + } + } + } +} diff --git a/crates/stackable-versioned-macros/tests/trybuild.rs b/crates/stackable-versioned-macros/tests/trybuild.rs index f2a215bd7..96000bfbb 100644 --- a/crates/stackable-versioned-macros/tests/trybuild.rs +++ b/crates/stackable-versioned-macros/tests/trybuild.rs @@ -23,6 +23,7 @@ mod inputs { // mod conversion_tracking_hints; // mod conversion_tracking; // mod crate_overrides; + // mod crate_overrides_only_kube; // mod docs; // mod downgrade_with; // mod enum_fields;