Skip to content

Commit 8e40d01

Browse files
committed
fix: Handle property hoisting for tagged enums (oneOf subschemas)
Also update function docs, and expected panic messages
1 parent cc6b20a commit 8e40d01

File tree

5 files changed

+78
-52
lines changed

5 files changed

+78
-52
lines changed

kube-core/src/schema/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,6 @@ impl Transform for StructuralSchemaRewriter {
274274
None => return,
275275
};
276276

277-
dbg!(&schema);
278-
279277
// Hoist parts of the schema to make it valid for k8s
280278
hoist_one_of_enum_with_unit_variants(&mut schema);
281279
hoist_any_of_subschema_with_a_nullable_variant(&mut schema);

kube-core/src/schema/transform_any_of.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,16 @@ fn optional_tagged_enum_with_unit_variants() {
8383
}
8484

8585

86-
/// Replace the schema with the non-null anyOf subschema when the only other subschema is the null schema.
86+
/// Replace the schema with the anyOf subschema and set to nullable when the
87+
/// only other subschema is the nullable entry.
8788
///
8889
/// Used for correcting the schema for optional tagged unit enums.
8990
/// The non-null subschema is hoisted, and nullable will be set to true.
9091
///
9192
/// This will return early without modifications unless:
92-
/// - There are exactly 2 `anyOf` subschemas
93-
/// - One subschema represents the `null` (has an enum with a null entry, and nullable set to true)
93+
/// - There are exactly 2 `anyOf` subschemas.
94+
/// - One subschema represents the nullability (ie: it has an enum with a single
95+
/// null entry, and nullable set to true).
9496
///
9597
/// NOTE: This should work regardless of whether other hoisting has been performed or not.
9698
pub(crate) fn hoist_any_of_subschema_with_a_nullable_variant(kube_schema: &mut SchemaObject) {

kube-core/src/schema/transform_one_of.rs

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,21 @@ fn tagged_enum_with_unit_variants() {
5656
}
5757

5858

59-
/// Replace a list of typed oneOf subschemas with a typed schema level enum
59+
/// Merge oneOf subschema enums and consts into a schema level enum.
6060
///
6161
/// Used for correcting the schema for tagged enums with unit variants.
62-
/// NOTE: Subschema descriptions are lost when they are combined into a single enum of the same type.
62+
///
63+
/// NOTE: Subschema descriptions are lost when they are combined into a single
64+
/// enum of the same type.
6365
///
6466
/// This will return early without modifications unless:
65-
/// - There are `oneOf` subschemas (not empty)
66-
/// - Each subschema contains an enum
67-
/// - Each subschema is typed
68-
/// - Each subschemas types is the same as the others
67+
/// - There are `oneOf` subschemas (not empty).
68+
/// - Each subschema contains an `enum` or `const`.
69+
///
70+
/// Subschemas must define a type, and they must be the same for all.
6971
///
70-
/// NOTE: This should work regardless of whether other hoisting has been performed or not.
72+
/// NOTE: This should work regardless of whether other hoisting has been
73+
/// performed or not.
7174
pub(crate) fn hoist_one_of_enum_with_unit_variants(kube_schema: &mut SchemaObject) {
7275
// Run some initial checks in case there is nothing to do
7376
let SchemaObject {
@@ -113,19 +116,23 @@ pub(crate) fn hoist_one_of_enum_with_unit_variants(kube_schema: &mut SchemaObjec
113116

114117
// For each `oneOf` entry, iterate over the `enum` and `const` values.
115118
// Panic on an entry that doesn't contain an `enum` or `const`.
116-
let new_enums = one_of.iter().flat_map(|schema| match schema {
117-
Schema::Object(SchemaObject {
118-
enum_values: Some(r#enum),
119-
..
120-
}) => r#enum.clone(),
121-
// Warning: The `const` check below must come after the enum check above.
122-
// Otherwise it will panic on a valid entry with an `enum`.
123-
Schema::Object(SchemaObject { other, .. }) => match other.get("const") {
124-
Some(r#const) => vec![r#const.clone()],
125-
None => panic!("oneOf variant did not provide \"enum\" or \"const\": {schema:#?}"),
126-
},
127-
Schema::Bool(_) => panic!("oneOf variants can not be of type boolean"),
128-
});
119+
let new_enums = one_of
120+
.iter()
121+
.flat_map(|schema| match schema {
122+
Schema::Object(SchemaObject {
123+
enum_values: Some(r#enum),
124+
..
125+
}) => r#enum.clone(),
126+
Schema::Object(SchemaObject { other, .. }) => other.get("const").cloned().into_iter().collect(),
127+
Schema::Bool(_) => panic!("oneOf variants can not be of type boolean"),
128+
})
129+
.collect::<Vec<_>>();
130+
131+
// If there are no enums in the oneOf subschemas, there is nothing more to do here.
132+
if new_enums.is_empty() {
133+
return;
134+
}
135+
129136
// Merge the enums (extend just to be safe)
130137
kube_schema.enum_values.get_or_insert_default().extend(new_enums);
131138

kube-core/src/schema/transform_properties.rs

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
use std::ops::DerefMut;
2-
3-
use crate::schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation};
1+
use crate::schema::{InstanceType, Schema, SchemaObject, SingleOrVec};
42

53
#[cfg(test)]
64
#[test]
@@ -342,13 +340,14 @@ fn invalid_untagged_enum_with_conflicting_variant_fields_after_one_of_hosting()
342340
hoist_properties_for_any_of_subschemas(&mut actual_converted_schema_object);
343341
}
344342

345-
/// Take subschema properties and insert them into the schema properties.
343+
/// Take oneOf or anyOf subschema properties and move them them into the schema
344+
/// properties.
346345
///
347346
/// Used for correcting the schema for serde untagged structural enums.
348-
/// NOTE: Due to the nature of "untagging", enum variant doc-comments are not preserved.
347+
/// NOTE: Doc-comments are not preserved for untagged enums.
349348
///
350349
/// This will return early without modifications unless:
351-
/// - There are `anyOf` subschemas
350+
/// - There are `oneOf` or `anyOf` subschemas
352351
/// - Each subschema has the type "object"
353352
///
354353
/// NOTE: This should work regardless of whether other hoisting has been performed or not.
@@ -363,21 +362,23 @@ pub(crate) fn hoist_properties_for_any_of_subschemas(kube_schema: &mut SchemaObj
363362
return;
364363
};
365364

366-
let SubschemaValidation {
367-
any_of: Some(any_of),
368-
one_of,
369-
} = subschemas.deref_mut()
370-
else {
371-
return;
365+
// Deal with both tagged and untagged enums.
366+
// Untagged enum descriptions will not be preserved.
367+
let (subschemas, preserve_description) = match (subschemas.any_of.as_mut(), subschemas.one_of.as_mut()) {
368+
(None, None) => return,
369+
(None, Some(one_of)) => (one_of, true),
370+
(Some(any_of), None) => (any_of, false),
371+
(Some(_), Some(_)) => panic!("oneOf and anyOf are mutually exclusive"),
372372
};
373373

374-
if any_of.is_empty() {
374+
375+
if subschemas.is_empty() {
375376
return;
376377
}
377378

378379
// Ensure we aren't looking at the one with a null
379380
// TODO (@NickLarsenNZ): Combine the logic with the function that covers the nullable anyOf
380-
if any_of.len() == 2 {
381+
if subschemas.len() == 2 {
381382
// This is the signature for the null variant, indicating the "other"
382383
// variant is the subschema that needs hoisting
383384
let null = serde_json::json!({
@@ -386,7 +387,7 @@ pub(crate) fn hoist_properties_for_any_of_subschemas(kube_schema: &mut SchemaObj
386387
});
387388

388389
// Return if one of the two entries are nulls
389-
for value in any_of
390+
for value in subschemas
390391
.iter()
391392
.map(|x| serde_json::to_value(x).expect("schema should be able to convert to JSON"))
392393
{
@@ -399,29 +400,47 @@ pub(crate) fn hoist_properties_for_any_of_subschemas(kube_schema: &mut SchemaObj
399400
// At this point, we can be reasonably sure we need operate on the schema.
400401
// TODO (@NickLarsenNZ): Return errors instead of panicking, leave panicking up to the infallible schemars::Transform
401402

402-
// There should not be any oneOf's adjacent to the anyOf
403-
if one_of.is_some() {
404-
panic!("oneOf is set when there is already an anyOf: {one_of:#?}");
405-
}
406-
407-
let subschemas = any_of
403+
let subschemas = subschemas
408404
.into_iter()
409405
.map(|schema| match schema {
410406
Schema::Object(schema_object) => schema_object,
411-
Schema::Bool(_) => panic!("oneOf variants can not be of type boolean"),
407+
Schema::Bool(_) => panic!("oneOf and anyOf variants cannot be of type boolean"),
412408
})
413409
.collect::<Vec<_>>();
414410

415411
for subschema in subschemas {
416-
// Drop description/type
417412
// This will clear out any objects that don't have required/properties fields (so that it appears as: {}).
418-
subschema.metadata.take();
413+
let metadata = subschema.metadata.take();
419414
subschema.instance_type.take();
420415

421416
// Set the schema type to object
422417
kube_schema.instance_type = Some(SingleOrVec::Single(Box::new(InstanceType::Object)));
423418

424419
if let Some(object) = subschema.object.as_deref_mut() {
420+
// Kubernetes doesn't allow variants to set additionalProperties
421+
object.additional_properties.take();
422+
423+
// For a tagged enum, we need to preserve the variant description
424+
if preserve_description {
425+
if object.properties.len() != 1 {
426+
// TODO (@NickLarsenNZ): Use assert_eq with error message (and in the other place)
427+
panic!("Expecting a subschema for a tagged enum variant, so there should only be one property");
428+
}
429+
430+
if let Schema::Object(x) = object
431+
.properties
432+
.values_mut()
433+
.next()
434+
.expect("it's there, trust sbernauer")
435+
{
436+
// I hope the new metadata doesn't have default set
437+
// I also hope that we didn't destroy some existing metadata.
438+
// Surely it would only exist in one or the other
439+
// See: https://github.com/kube-rs/kube/blob/98bfbe3d7923321a16ccde9e690fd2ce8c7efc32/kube-core/src/schema.rs#L409-L426
440+
x.metadata = metadata
441+
};
442+
}
443+
425444
// If properties are set, hoist them to the schema properties.
426445
// This will panic if duplicate properties are encountered that do not have the same shape.
427446
// That can happen when the untagged enum variants each refer to structs which contain the same field name.

kube-derive/tests/crd_mixed_enum_test.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@ fn print_crds() {
5353
}
5454

5555
#[test]
56-
#[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."]
56+
#[should_panic = "oneOf variants must all have the same type"]
5757
fn invalid_enum_1() {
5858
InvalidEnum1::crd();
5959
}
6060

6161
#[test]
62-
#[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."]
62+
#[should_panic = "oneOf variants must all have the same type"]
6363
fn invalid_enum_2() {
6464
InvalidEnum2::crd();
6565
}
@@ -222,7 +222,7 @@ fn valid_enum_4() {
222222
}
223223

224224
#[test]
225-
#[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."]
225+
#[should_panic = "oneOf variants must all have the same type"]
226226
fn struct_with_enum_1() {
227227
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
228228
#[kube(group = "clux.dev", version = "v1", kind = "Foo")]

0 commit comments

Comments
 (0)