diff --git a/kube-core/src/schema.rs b/kube-core/src/schema.rs index 479918a8e..c426da3b4 100644 --- a/kube-core/src/schema.rs +++ b/kube-core/src/schema.rs @@ -7,8 +7,11 @@ use schemars::{transform::Transform, JsonSchema}; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::collections::{btree_map::Entry, BTreeMap, BTreeSet}; +use serde_json::{json, Value}; +use std::{ + collections::{btree_map::Entry, BTreeMap, BTreeSet}, + ops::Deref as _, +}; /// schemars [`Visitor`] that rewrites a [`Schema`] to conform to Kubernetes' "structural schema" rules /// @@ -246,16 +249,310 @@ enum SingleOrVec { Vec(Vec), } +// #[cfg(test)] +// mod test { +// use assert_json_diff::assert_json_eq; +// use schemars::{json_schema, schema_for, JsonSchema}; +// use serde::{Deserialize, Serialize}; + +// use super::*; + +// /// A very simple enum with unit variants, and no comments +// #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +// enum NormalEnumNoComments { +// A, +// B, +// } + +// /// A very simple enum with unit variants, and comments +// #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +// enum NormalEnum { +// /// First variant +// A, +// /// Second variant +// B, + +// // No doc-comments on these variants +// C, +// D, +// } + +// #[test] +// fn schema_for_enum_without_comments() { +// let schemars_schema = schema_for!(NormalEnumNoComments); + +// assert_json_eq!( +// schemars_schema, +// // replace the json_schema with this to get the full output. +// // serde_json::json!(42) +// json_schema!( +// { +// "$schema": "https://json-schema.org/draft/2020-12/schema", +// "description": "A very simple enum with unit variants, and no comments", +// "enum": [ +// "A", +// "B" +// ], +// "title": "NormalEnumNoComments", +// "type": "string" +// } +// ) +// ); + +// let kube_schema: crate::schema::Schema = +// schemars_schema_to_kube_schema(schemars_schema.clone()).unwrap(); + +// let hoisted_kube_schema = hoist_one_of_enum(kube_schema.clone()); + +// // No hoisting needed +// assert_json_eq!(hoisted_kube_schema, kube_schema); +// } + +// #[test] +// fn schema_for_enum_with_comments() { +// let schemars_schema = schema_for!(NormalEnum); + +// assert_json_eq!( +// schemars_schema, +// // replace the json_schema with this to get the full output. +// // serde_json::json!(42) +// json_schema!( +// { +// "$schema": "https://json-schema.org/draft/2020-12/schema", +// "description": "A very simple enum with unit variants, and comments", +// "oneOf": [ +// { +// "enum": [ +// "C", +// "D" +// ], +// "type": "string" +// }, +// { +// "const": "A", +// "description": "First variant", +// "type": "string" +// }, +// { +// "const": "B", +// "description": "Second variant", +// "type": "string" +// } +// ], +// "title": "NormalEnum" +// } +// ) +// ); + + +// let kube_schema: crate::schema::Schema = +// schemars_schema_to_kube_schema(schemars_schema.clone()).unwrap(); + +// let hoisted_kube_schema = hoist_one_of_enum(kube_schema.clone()); + +// assert_ne!( +// hoisted_kube_schema, kube_schema, +// "Hoisting was performed, so hoisted_kube_schema != kube_schema" +// ); +// assert_json_eq!( +// hoisted_kube_schema, +// json_schema!( +// { +// "$schema": "https://json-schema.org/draft/2020-12/schema", +// "description": "A very simple enum with unit variants, and comments", +// "type": "string", +// "enum": [ +// "C", +// "D", +// "A", +// "B" +// ], +// "title": "NormalEnum" +// } +// ) +// ); +// } +// } + +#[cfg(test)] +fn schemars_schema_to_kube_schema(incoming: schemars::Schema) -> Result { + serde_json::from_value(incoming.to_value()) +} + +/// Hoist `oneOf` into top level `enum`. +/// +/// This will move all `enum` variants and `const` values under `oneOf` into a single top level `enum` along with `type`. +/// It will panic if there are anomalies, like differences in `type` values, or lack of `enum` or `const` fields in the `oneOf` entries. +/// +/// Note: variant descriptions will be lost in the process, and the original `oneOf` will be erased. +/// +// Note: This function is heavily documented to express intent. It is intended to help developers +// make adjustments for future Schemars changes. +fn hoist_one_of_enum(incoming: SchemaObject) -> SchemaObject { + // Run some initial checks in case there is nothing to do + let SchemaObject { + subschemas: Some(subschemas), + .. + } = &incoming + else { + return incoming; + }; + + let SubschemaValidation { + one_of: Some(one_of), .. + } = subschemas.deref() + else { + return incoming; + }; + + if one_of.is_empty() { + return incoming; + } + + // At this point, we need to create a new Schema and hoist the `oneOf` + // variants' `enum`/`const` values up into a parent `enum`. + let mut new_schema = incoming.clone(); + if let SchemaObject { + subschemas: Some(new_subschemas), + instance_type: new_instance_type, + enum_values: new_enum_values, + .. + } = &mut new_schema + { + // For each `oneOf`, get the `type`. + // Panic if it has no `type`, or if the entry is a boolean. + let mut types = one_of.iter().map(|obj| match obj { + Schema::Object(SchemaObject { + instance_type: Some(r#type), + .. + }) => r#type, + // TODO (@NickLarsenNZ): Is it correct that JSON Schema oneOf must have a type? + Schema::Object(_) => panic!("oneOf variants need to define a type!: {obj:?}"), + Schema::Bool(_) => panic!("oneOf variants can not be of type boolean"), + }); + + // Get the first `type` value, then panic if any subsequent `type` values differ. + let hoisted_instance_type = types + .next() + .expect("oneOf must have at least one variant - we already checked that"); + // TODO (@NickLarsenNZ): Didn't sbernauer say that the types + if types.any(|t| t != hoisted_instance_type) { + panic!("All oneOf variants must have the same type"); + } + + *new_instance_type = Some(hoisted_instance_type.clone()); + + // For each `oneOf` entry, iterate over the `enum` and `const` values. + // Panic on an entry that doesn't contain an `enum` or `const`. + let new_enums = one_of.iter().flat_map(|obj| match obj { + Schema::Object(SchemaObject { + enum_values: Some(r#enum), + .. + }) => r#enum.clone(), + // Warning: The `const` check below must come after the enum check above. + // Otherwise it will panic on a valid entry with an `enum`. + Schema::Object(SchemaObject { other, .. }) => match other.get("const") { + Some(r#const) => vec![r#const.clone()], + None => panic!("oneOf variant did not provide \"enum\" or \"const\": {obj:?}"), + }, + Schema::Bool(_) => panic!("oneOf variants can not be of type boolean"), + }); + + // Just in case there were existing enum values, add to them. + // TODO (@NickLarsenNZ): Check if `oneOf` and `enum` are mutually exclusive for a valid spec. + new_enum_values.get_or_insert_default().extend(new_enums); + + // We can clear out the existing oneOf's, since they will be hoisted below. + new_subschemas.one_of = None; + } + + new_schema +} + +// if anyOf with 2 entries, and one is nullable with enum that is [null], +// then hoist nullable, description, type, enum from the other entry. +// set anyOf to None +fn hoist_any_of_option_enum(incoming: SchemaObject) -> SchemaObject { + // Run some initial checks in case there is nothing to do + let SchemaObject { + subschemas: Some(subschemas), + .. + } = &incoming + else { + return incoming; + }; + + let SubschemaValidation { + any_of: Some(any_of), .. + } = subschemas.deref() + else { + return incoming; + }; + + if any_of.len() != 2 { + return incoming; + }; + + // This is the signature of an Optional enum that needs hoisting + let null = json!({ + "enum": [null], + "nullable": true + }); + + // iter through any_of for matching null + let results: [bool; 2] = any_of + .iter() + .map(|x| serde_json::to_value(x).expect("schema should be able to convert to JSON")) + .map(|x| x == null) + .collect::>() + .try_into() + .expect("there should be exactly 2 elements. We checked earlier"); + + let to_hoist = match results { + [true, true] => panic!("Too many nulls, not enough drinks"), + [true, false] => &any_of[1], + [false, true] => &any_of[0], + [false, false] => return incoming, + }; + + // my goodness! + let Schema::Object(to_hoist) = to_hoist else { + panic!("Somehow we have stumbled across a bool schema"); + }; + + let mut new_schema = incoming.clone(); + + let mut new_metadata = incoming.metadata.clone().unwrap_or_default(); + new_metadata.description = to_hoist.metadata.as_ref().and_then(|m| m.description.clone()); + + new_schema.metadata = Some(new_metadata); + new_schema.instance_type = to_hoist.instance_type.clone(); + new_schema.enum_values = to_hoist.enum_values.clone(); + new_schema.other["nullable"] = true.into(); + + new_schema + .subschemas + .as_mut() + .expect("we have asserted that there is any_of") + .any_of = None; + + new_schema +} + + impl Transform for StructuralSchemaRewriter { fn transform(&mut self, transform_schema: &mut schemars::Schema) { schemars::transform::transform_subschemas(self, transform_schema); - let mut schema: SchemaObject = match serde_json::from_value(transform_schema.clone().to_value()).ok() - { + // TODO (@NickLarsenNZ): Replace with conversion function + let schema: SchemaObject = match serde_json::from_value(transform_schema.clone().to_value()).ok() { Some(schema) => schema, None => return, }; - + let schema = hoist_one_of_enum(schema); + let schema = hoist_any_of_option_enum(schema); + // todo: let schema = strip_any_of_empty_object_entry(schema); + let mut schema = schema; if let Some(subschemas) = &mut schema.subschemas { if let Some(one_of) = subschemas.one_of.as_mut() { // Tagged enums are serialized using `one_of` @@ -394,6 +691,17 @@ fn hoist_subschema_properties( variant_obj.additional_properties = None; merge_metadata(instance_type, variant_type.take()); + } else if let Schema::Object(SchemaObject { + object: None, + instance_type: variant_type, + metadata, + .. + }) = variant + { + if *variant_type == Some(SingleOrVec::Single(Box::new(InstanceType::Object))) { + *variant_type = None; + *metadata = None; + } } } } diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index 443653576..bc5a173a9 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -904,15 +904,17 @@ mod tests { #[test] fn test_derive_crd() { - let path = env::current_dir().unwrap().join("tests").join("crd_enum_test.rs"); - let file = fs::File::open(path).unwrap(); - runtime_macros::emulate_derive_macro_expansion(file, &[("CustomResource", derive)]).unwrap(); - - let path = env::current_dir() - .unwrap() - .join("tests") - .join("crd_schema_test.rs"); - let file = fs::File::open(path).unwrap(); - runtime_macros::emulate_derive_macro_expansion(file, &[("CustomResource", derive)]).unwrap(); + let files = [ + "crd_complex_enum_tests.rs", + "crd_mixed_enum_test.rs", + "crd_schema_test.rs", + "crd_top_level_enum_test.rs", + ]; + + for file in files { + let path = env::current_dir().unwrap().join("tests").join(file); + let file = fs::File::open(path).unwrap(); + runtime_macros::emulate_derive_macro_expansion(file, &[("CustomResource", derive)]).unwrap(); + } } } diff --git a/kube-derive/tests/crd_complex_enum_tests.rs b/kube-derive/tests/crd_complex_enum_tests.rs new file mode 100644 index 000000000..a25f01cad --- /dev/null +++ b/kube-derive/tests/crd_complex_enum_tests.rs @@ -0,0 +1,484 @@ +#![allow(missing_docs)] +use assert_json_diff::assert_json_eq; +use kube::{CustomResource, CustomResourceExt}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +/// A very simple enum with empty variants +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +enum NormalEnum { + /// First variant + A, + /// Second variant + B, + + // No doc-comments on these variants + C, + D, +} + +/// An untagged enum with a nested enum inside +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[serde(untagged)] +enum UntaggedEnum { + /// Used in case the `one` field of tpye [`u32`] is present + A { one: String }, + /// Used in case the `two` field of type [`NormalEnum`] is present + B { two: NormalEnum }, + /// Used in case no fields are present + C {}, +} + +/// Put a [`UntaggedEnum`] behind `#[serde(flatten)]` +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +struct FlattenedUntaggedEnum { + #[serde(flatten)] + inner: UntaggedEnum, +} + +#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[kube(group = "clux.dev", version = "v1", kind = "NormalEnumTest")] +struct NormalEnumTestSpec { + foo: NormalEnum, +} + +#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[kube(group = "clux.dev", version = "v1", kind = "OptionalEnumTest")] +struct OptionalEnumTestSpec { + foo: Option, +} + +#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[kube(group = "clux.dev", version = "v1", kind = "UntaggedEnumTest")] +struct UntaggedEnumTestSpec { + foo: UntaggedEnum, +} + +#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[kube(group = "clux.dev", version = "v1", kind = "OptionalUntaggedEnumTest")] +struct OptionalUntaggedEnumTestSpec { + foo: Option, +} + +#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[kube(group = "clux.dev", version = "v1", kind = "FlattenedUntaggedEnumTest")] +struct FlattenedUntaggedEnumTestSpec { + foo: FlattenedUntaggedEnum, +} + +/// Use `cargo test --package kube-derive print_crds -- --nocapture` to get the CRDs as YAML. +/// Afterwards you can use `kubectl apply -f -` to see if they are valid CRDs. +#[test] +fn print_crds() { + println!("{}", serde_yaml::to_string(&NormalEnumTest::crd()).unwrap()); + println!("---"); + println!("{}", serde_yaml::to_string(&OptionalEnumTest::crd()).unwrap()); + println!("---"); + println!("{}", serde_yaml::to_string(&UntaggedEnumTest::crd()).unwrap()); + println!("---"); + println!( + "{}", + serde_yaml::to_string(&OptionalUntaggedEnumTest::crd()).unwrap() + ); + println!("---"); + println!( + "{}", + serde_yaml::to_string(&FlattenedUntaggedEnumTest::crd()).unwrap() + ); +} + +#[test] +fn normal_enum() { + assert_json_eq!( + NormalEnumTest::crd(), + json!( + { + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "normalenumtests.clux.dev" + }, + "spec": { + "group": "clux.dev", + "names": { + "categories": [], + "kind": "NormalEnumTest", + "plural": "normalenumtests", + "shortNames": [], + "singular": "normalenumtest" + }, + "scope": "Cluster", + "versions": [ + { + "additionalPrinterColumns": [], + "name": "v1", + "schema": { + "openAPIV3Schema": { + "description": "Auto-generated derived type for NormalEnumTestSpec via `CustomResource`", + "properties": { + "spec": { + "properties": { + "foo": { + "description": "A very simple enum with empty variants", + "enum": [ + "C", + "D", + "A", + "B" + ], + "type": "string" + } + }, + "required": [ + "foo" + ], + "type": "object" + } + }, + "required": [ + "spec" + ], + "title": "NormalEnumTest", + "type": "object" + } + }, + "served": true, + "storage": true, + "subresources": {} + } + ] + } + } + ) + ); +} + +#[test] +fn optional_enum() { + assert_json_eq!( + OptionalEnumTest::crd(), + json!( + { + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "optionalenumtests.clux.dev" + }, + "spec": { + "group": "clux.dev", + "names": { + "categories": [], + "kind": "OptionalEnumTest", + "plural": "optionalenumtests", + "shortNames": [], + "singular": "optionalenumtest" + }, + "scope": "Cluster", + "versions": [ + { + "additionalPrinterColumns": [], + "name": "v1", + "schema": { + "openAPIV3Schema": { + "description": "Auto-generated derived type for OptionalEnumTestSpec via `CustomResource`", + "properties": { + "spec": { + "properties": { + "foo": { + "description": "A very simple enum with empty variants", + "enum": [ + "C", + "D", + "A", + "B" + ], + "nullable": true, + "type": "string" + } + }, + "type": "object" + } + }, + "required": [ + "spec" + ], + "title": "OptionalEnumTest", + "type": "object" + } + }, + "served": true, + "storage": true, + "subresources": {} + } + ] + } + } + ) + ); +} + +#[test] +fn untagged_enum() { + assert_json_eq!( + UntaggedEnumTest::crd(), + json!( + { + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "untaggedenumtests.clux.dev" + }, + "spec": { + "group": "clux.dev", + "names": { + "categories": [], + "kind": "UntaggedEnumTest", + "plural": "untaggedenumtests", + "shortNames": [], + "singular": "untaggedenumtest" + }, + "scope": "Cluster", + "versions": [ + { + "additionalPrinterColumns": [], + "name": "v1", + "schema": { + "openAPIV3Schema": { + "description": "Auto-generated derived type for UntaggedEnumTestSpec via `CustomResource`", + "properties": { + "spec": { + "properties": { + "foo": { + "anyOf": [ + { + "required": [ + "one" + ] + }, + { + "required": [ + "two" + ] + }, + {} + ], + "description": "An untagged enum with a nested enum inside", + "properties": { + "one": { + "description": "Used in case the `one` field of tpye [`u32`] is present", + "type": "string" + }, + "two": { + "description": "Used in case the `two` field of type [`NormalEnum`] is present", + "enum": [ + "C", + "D", + "A", + "B" + ], + "type": "string" + } + }, + "type": "object" + } + }, + "required": [ + "foo" + ], + "type": "object" + } + }, + "required": [ + "spec" + ], + "title": "UntaggedEnumTest", + "type": "object" + } + }, + "served": true, + "storage": true, + "subresources": {} + } + ] + } + } + ) + ); +} + +#[test] +fn optional_untagged_enum() { + assert_json_eq!( + OptionalUntaggedEnumTest::crd(), + json!( + { + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "optionaluntaggedenumtests.clux.dev" + }, + "spec": { + "group": "clux.dev", + "names": { + "categories": [], + "kind": "OptionalUntaggedEnumTest", + "plural": "optionaluntaggedenumtests", + "shortNames": [], + "singular": "optionaluntaggedenumtest" + }, + "scope": "Cluster", + "versions": [ + { + "additionalPrinterColumns": [], + "name": "v1", + "schema": { + "openAPIV3Schema": { + "description": "Auto-generated derived type for OptionalUntaggedEnumTestSpec via `CustomResource`", + "properties": { + "spec": { + "properties": { + "foo": { + "anyOf": [ + { + "required": [ + "one" + ] + }, + { + "required": [ + "two" + ] + }, + {} + ], + "description": "An untagged enum with a nested enum inside", + "nullable": true, + "properties": { + "one": { + "description": "Used in case the `one` field of tpye [`u32`] is present", + "type": "string" + }, + "two": { + "description": "Used in case the `two` field of type [`NormalEnum`] is present", + "enum": [ + "C", + "D", + "A", + "B" + ], + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "required": [ + "spec" + ], + "title": "OptionalUntaggedEnumTest", + "type": "object" + } + }, + "served": true, + "storage": true, + "subresources": {} + } + ] + } + } + ) + ); +} + +#[test] +fn flattened_untagged_enum() { + assert_json_eq!( + FlattenedUntaggedEnumTest::crd(), + json!( + { + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "flatteneduntaggedenumtests.clux.dev" + }, + "spec": { + "group": "clux.dev", + "names": { + "categories": [], + "kind": "FlattenedUntaggedEnumTest", + "plural": "flatteneduntaggedenumtests", + "shortNames": [], + "singular": "flatteneduntaggedenumtest" + }, + "scope": "Cluster", + "versions": [ + { + "additionalPrinterColumns": [], + "name": "v1", + "schema": { + "openAPIV3Schema": { + "description": "Auto-generated derived type for FlattenedUntaggedEnumTestSpec via `CustomResource`", + "properties": { + "spec": { + "properties": { + "foo": { + "anyOf": [ + { + "required": [ + "one" + ] + }, + { + "required": [ + "two" + ] + }, + {} + ], + "description": "Put a [`UntaggedEnum`] behind `#[serde(flatten)]`", + "properties": { + "one": { + "description": "Used in case the `one` field of tpye [`u32`] is present", + "type": "string" + }, + "two": { + "description": "Used in case the `two` field of type [`NormalEnum`] is present", + "enum": [ + "C", + "D", + "A", + "B" + ], + "type": "string" + } + }, + "type": "object" + } + }, + "required": [ + "foo" + ], + "type": "object" + } + }, + "required": [ + "spec" + ], + "title": "FlattenedUntaggedEnumTest", + "type": "object" + } + }, + "served": true, + "storage": true, + "subresources": {} + } + ] + } + } + ) + ); +} diff --git a/kube-derive/tests/crd_mixed_enum_test.rs b/kube-derive/tests/crd_mixed_enum_test.rs new file mode 100644 index 000000000..08dfb8249 --- /dev/null +++ b/kube-derive/tests/crd_mixed_enum_test.rs @@ -0,0 +1,234 @@ +#![allow(missing_docs)] +use assert_json_diff::assert_json_eq; +use kube::{CustomResource, CustomResourceExt}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +/// This enum is invalid, as "plain" (string) variants are mixed with object variants +#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[kube(group = "clux.dev", version = "v1", kind = "InvalidEnum1")] +enum InvalidEnum1Spec { + /// Unit variant (represented as string) + A, + /// Takes an [`u32`] (represented as object) + B(u32), +} + +/// This enum is invalid, as "plain" (string) variants are mixed with object variants +#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[kube(group = "clux.dev", version = "v1", kind = "InvalidEnum2")] +enum InvalidEnum2Spec { + /// Unit variant (represented as string) + A, + /// Takes a single field (represented as object) + B { inner: u32 }, +} + +/// This enum is valid, as all variants are objects +#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[kube(group = "clux.dev", version = "v1", kind = "ValidEnum3")] +enum ValidEnum3Spec { + /// Takes an [`String`] (represented as object) + A(String), + /// Takes an [`u32`] (represented as object) + B(u32), +} + +// This enum intentionally has no documentation to increase test coverage! +#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[kube(group = "clux.dev", version = "v1", kind = "ValidEnum4")] +enum ValidEnum4Spec { + A(String), + B { inner: u32 }, +} + +/// Use `cargo test --package kube-derive print_crds -- --nocapture` to get the CRDs as YAML. +/// Afterwards you can use `kubectl apply -f -` to see if they are valid CRDs. +#[test] +fn print_crds() { + println!("{}", serde_yaml::to_string(&ValidEnum3::crd()).unwrap()); + println!("---"); + println!("{}", serde_yaml::to_string(&ValidEnum4::crd()).unwrap()); +} + +#[test] +#[should_panic = "Enum variant set [String(\"A\")] has type Single(String) but was already defined as Some(Single(Object)). The instance type must be equal for all subschema variants."] +fn invalid_enum_1() { + InvalidEnum1::crd(); +} + +#[test] +#[should_panic = "Enum variant set [String(\"A\")] has type Single(String) but was already defined as Some(Single(Object)). The instance type must be equal for all subschema variants."] +fn invalid_enum_2() { + InvalidEnum2::crd(); +} + +#[test] +fn valid_enum_3() { + assert_json_eq!( + ValidEnum3::crd(), + json!( + { + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "validenum3s.clux.dev" + }, + "spec": { + "group": "clux.dev", + "names": { + "categories": [], + "kind": "ValidEnum3", + "plural": "validenum3s", + "shortNames": [], + "singular": "validenum3" + }, + "scope": "Cluster", + "versions": [ + { + "additionalPrinterColumns": [], + "name": "v1", + "schema": { + "openAPIV3Schema": { + "description": "Auto-generated derived type for ValidEnum3Spec via `CustomResource`", + "properties": { + "spec": { + "description": "This enum is valid, as all variants are objects", + "oneOf": [ + { + "required": [ + "A" + ] + }, + { + "required": [ + "B" + ] + } + ], + "properties": { + "A": { + "description": "Takes an [`String`] (represented as object)", + "type": "string" + }, + "B": { + "description": "Takes an [`u32`] (represented as object)", + "format": "uint32", + "minimum": 0.0, + "type": "integer" + } + }, + "type": "object" + } + }, + "required": [ + "spec" + ], + "title": "ValidEnum3", + "type": "object" + } + }, + "served": true, + "storage": true, + "subresources": {} + } + ] + } + } + ) + ); +} + +#[test] +fn valid_enum_4() { + assert_json_eq!( + ValidEnum4::crd(), + json!( + { + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "validenum4s.clux.dev" + }, + "spec": { + "group": "clux.dev", + "names": { + "categories": [], + "kind": "ValidEnum4", + "plural": "validenum4s", + "shortNames": [], + "singular": "validenum4" + }, + "scope": "Cluster", + "versions": [ + { + "additionalPrinterColumns": [], + "name": "v1", + "schema": { + "openAPIV3Schema": { + "description": "Auto-generated derived type for ValidEnum4Spec via `CustomResource`", + "properties": { + "spec": { + "oneOf": [ + { + "required": [ + "A" + ] + }, + { + "required": [ + "B" + ] + } + ], + "properties": { + "A": { + "type": "string" + }, + "B": { + "properties": { + "inner": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "inner" + ], + "type": "object" + } + }, + "type": "object" + } + }, + "required": [ + "spec" + ], + "title": "ValidEnum4", + "type": "object" + } + }, + "served": true, + "storage": true, + "subresources": {} + } + ] + } + } + ) + ); +} + +#[test] +#[should_panic = "Enum variant set [String(\"A\")] has type Single(String) but was already defined as Some(Single(Object)). The instance type must be equal for all subschema variants."] +fn struct_with_enum_1() { + #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] + #[kube(group = "clux.dev", version = "v1", kind = "Foo")] + struct FooSpec { + foo: InvalidEnum1, + } + + Foo::crd(); +} diff --git a/kube-derive/tests/crd_enum_test.rs b/kube-derive/tests/crd_top_level_enum_test.rs similarity index 100% rename from kube-derive/tests/crd_enum_test.rs rename to kube-derive/tests/crd_top_level_enum_test.rs diff --git a/kube-derive/tests/ui/union_fails.stderr b/kube-derive/tests/ui/union_fails.stderr index 3bc077716..ae002ca36 100644 --- a/kube-derive/tests/ui/union_fails.stderr +++ b/kube-derive/tests/ui/union_fails.stderr @@ -7,7 +7,7 @@ error: Unions can not #[derive(CustomResource)] error: Serde does not support derive for unions --> tests/ui/union_fails.rs:8:1 | -8 | / union FooSpec { -9 | | int: u32, + 8 | / union FooSpec { + 9 | | int: u32, 10 | | } | |_^