Skip to content

Commit 3033e0f

Browse files
authored
Merge pull request #1028 from sbernauer/support-untagged-enums
Add support for untagged enums in CRDs
2 parents a26e7f3 + 9ad4b36 commit 3033e0f

File tree

2 files changed

+171
-63
lines changed

2 files changed

+171
-63
lines changed

kube-core/src/schema.rs

+92-60
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ use std::collections::btree_map::Entry;
88
#[allow(unused_imports)] use schemars::gen::SchemaSettings;
99

1010
use schemars::{
11-
schema::{Metadata, ObjectValidation, Schema, SchemaObject},
11+
schema::{InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SingleOrVec},
1212
visit::Visitor,
1313
};
1414

1515
/// schemars [`Visitor`] that rewrites a [`Schema`] to conform to Kubernetes' "structural schema" rules
1616
///
1717
/// The following two transformations are applied
1818
/// * Rewrite enums from `oneOf` to `object`s with multiple variants ([schemars#84](https://github.com/GREsau/schemars/issues/84))
19+
/// * Rewrite untagged enums from `anyOf` to `object`s with multiple variants ([kube#1028](https://github.com/kube-rs/kube/pull/1028))
1920
/// * Rewrite `additionalProperties` from `#[serde(flatten)]` to `x-kubernetes-preserve-unknown-fields` ([kube#844](https://github.com/kube-rs/kube/issues/844))
2021
///
2122
/// This is used automatically by `kube::derive`'s `#[derive(CustomResource)]`,
@@ -31,72 +32,26 @@ pub struct StructuralSchemaRewriter;
3132
impl Visitor for StructuralSchemaRewriter {
3233
fn visit_schema_object(&mut self, schema: &mut schemars::schema::SchemaObject) {
3334
schemars::visit::visit_schema_object(self, schema);
35+
3436
if let Some(one_of) = schema
3537
.subschemas
3638
.as_mut()
3739
.and_then(|subschemas| subschemas.one_of.as_mut())
3840
{
39-
let common_obj = schema
40-
.object
41-
.get_or_insert_with(|| Box::new(ObjectValidation::default()));
42-
for variant in one_of {
43-
if let Schema::Object(SchemaObject {
44-
instance_type: variant_type,
45-
object: Some(variant_obj),
46-
metadata: variant_metadata,
47-
..
48-
}) = variant
49-
{
50-
if let Some(variant_metadata) = variant_metadata {
51-
// Move enum variant description from oneOf clause to its corresponding property
52-
if let Some(description) = std::mem::take(&mut variant_metadata.description) {
53-
if let Some(Schema::Object(variant_object)) =
54-
only_item(variant_obj.properties.values_mut())
55-
{
56-
let metadata = variant_object
57-
.metadata
58-
.get_or_insert_with(|| Box::new(Metadata::default()));
59-
metadata.description = Some(description);
60-
}
61-
}
62-
}
63-
64-
// Move all properties
65-
let variant_properties = std::mem::take(&mut variant_obj.properties);
66-
for (property_name, property) in variant_properties {
67-
match common_obj.properties.entry(property_name) {
68-
Entry::Occupied(entry) => panic!(
69-
"property {:?} is already defined in another enum variant",
70-
entry.key()
71-
),
72-
Entry::Vacant(entry) => {
73-
entry.insert(property);
74-
}
75-
}
76-
}
77-
78-
// Kubernetes doesn't allow variants to set additionalProperties
79-
variant_obj.additional_properties = None;
41+
// Tagged enums are serialized using `one_of`
42+
hoist_subschema_properties(one_of, &mut schema.object, &mut schema.instance_type);
43+
}
8044

81-
// Try to merge metadata
82-
match (&mut schema.instance_type, variant_type.take()) {
83-
(_, None) => {}
84-
(common_type @ None, variant_type) => {
85-
*common_type = variant_type;
86-
}
87-
(Some(common_type), Some(variant_type)) => {
88-
if *common_type != variant_type {
89-
panic!(
90-
"variant defined type {:?}, conflicting with existing type {:?}",
91-
variant_type, common_type
92-
);
93-
}
94-
}
95-
}
96-
}
97-
}
45+
if let Some(any_of) = schema
46+
.subschemas
47+
.as_mut()
48+
.and_then(|subschemas| subschemas.any_of.as_mut())
49+
{
50+
// Untagged enums are serialized using `any_of`
51+
hoist_subschema_properties(any_of, &mut schema.object, &mut schema.instance_type);
9852
}
99-
// check for maps without with properties (i.e. flattnened maps)
53+
54+
// check for maps without with properties (i.e. flattened maps)
10055
// and allow these to persist dynamically
10156
if let Some(object) = &mut schema.object {
10257
if !object.properties.is_empty()
@@ -111,10 +66,87 @@ impl Visitor for StructuralSchemaRewriter {
11166
}
11267
}
11368

69+
/// Bring all property definitions from subschemas up to the root schema,
70+
/// since Kubernetes doesn't allow subschemas to define properties.
71+
fn hoist_subschema_properties(
72+
subschemas: &mut Vec<Schema>,
73+
common_obj: &mut Option<Box<ObjectValidation>>,
74+
instance_type: &mut Option<SingleOrVec<InstanceType>>,
75+
) {
76+
let common_obj = common_obj.get_or_insert_with(|| Box::new(ObjectValidation::default()));
77+
78+
for variant in subschemas {
79+
if let Schema::Object(SchemaObject {
80+
instance_type: variant_type,
81+
object: Some(variant_obj),
82+
metadata: variant_metadata,
83+
..
84+
}) = variant
85+
{
86+
if let Some(variant_metadata) = variant_metadata {
87+
// Move enum variant description from oneOf clause to its corresponding property
88+
if let Some(description) = std::mem::take(&mut variant_metadata.description) {
89+
if let Some(Schema::Object(variant_object)) =
90+
only_item(variant_obj.properties.values_mut())
91+
{
92+
let metadata = variant_object
93+
.metadata
94+
.get_or_insert_with(|| Box::new(Metadata::default()));
95+
metadata.description = Some(description);
96+
}
97+
}
98+
}
99+
100+
// Move all properties
101+
let variant_properties = std::mem::take(&mut variant_obj.properties);
102+
for (property_name, property) in variant_properties {
103+
match common_obj.properties.entry(property_name) {
104+
Entry::Vacant(entry) => {
105+
entry.insert(property);
106+
}
107+
Entry::Occupied(entry) => {
108+
if &property != entry.get() {
109+
panic!("Property {:?} has the schema {:?} but was already defined as {:?} in another subschema. The schemas for a property used in multiple subschemas must be identical",
110+
entry.key(),
111+
&property,
112+
entry.get());
113+
}
114+
}
115+
}
116+
}
117+
118+
// Kubernetes doesn't allow variants to set additionalProperties
119+
variant_obj.additional_properties = None;
120+
121+
merge_metadata(instance_type, variant_type.take());
122+
}
123+
}
124+
}
125+
114126
fn only_item<I: Iterator>(mut i: I) -> Option<I::Item> {
115127
let item = i.next()?;
116128
if i.next().is_some() {
117129
return None;
118130
}
119131
Some(item)
120132
}
133+
134+
fn merge_metadata(
135+
instance_type: &mut Option<SingleOrVec<InstanceType>>,
136+
variant_type: Option<SingleOrVec<InstanceType>>,
137+
) {
138+
match (instance_type, variant_type) {
139+
(_, None) => {}
140+
(common_type @ None, variant_type) => {
141+
*common_type = variant_type;
142+
}
143+
(Some(common_type), Some(variant_type)) => {
144+
if *common_type != variant_type {
145+
panic!(
146+
"variant defined type {:?}, conflicting with existing type {:?}",
147+
variant_type, common_type
148+
);
149+
}
150+
}
151+
}
152+
}

kube-derive/tests/crd_schema_test.rs

+79-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#![recursion_limit = "256"]
2+
13
use chrono::{DateTime, NaiveDateTime, Utc};
24
use kube_derive::CustomResource;
35
use schemars::JsonSchema;
@@ -37,8 +39,11 @@ struct FooSpec {
3739
// Using feature `chrono`
3840
timestamp: DateTime<Utc>,
3941

40-
/// This is a complex enum
42+
/// This is a complex enum with a description
4143
complex_enum: ComplexEnum,
44+
45+
/// This is a untagged enum with a description
46+
untagged_enum_person: UntaggedEnumPerson,
4247
}
4348

4449
fn default_value() -> String {
@@ -69,6 +74,40 @@ enum ComplexEnum {
6974
VariantThree {},
7075
}
7176

77+
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
78+
#[serde(rename_all = "camelCase")]
79+
#[serde(untagged)]
80+
enum UntaggedEnumPerson {
81+
SexAndAge(SexAndAge),
82+
SexAndDateOfBirth(SexAndDateOfBirth),
83+
}
84+
85+
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
86+
#[serde(rename_all = "camelCase")]
87+
struct SexAndAge {
88+
/// Sex of the person
89+
sex: Sex,
90+
/// Age of the person in years
91+
age: i32,
92+
}
93+
94+
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
95+
#[serde(rename_all = "camelCase")]
96+
struct SexAndDateOfBirth {
97+
/// Sex of the person
98+
sex: Sex,
99+
/// Date of birth of the person as ISO 8601 date
100+
date_of_birth: String,
101+
}
102+
103+
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
104+
#[serde(rename_all = "PascalCase")]
105+
enum Sex {
106+
Female,
107+
Male,
108+
Other,
109+
}
110+
72111
#[test]
73112
fn test_crd_name() {
74113
use kube::core::CustomResourceExt;
@@ -93,6 +132,10 @@ fn test_serialized_matches_expected() {
93132
nullable_with_default: None,
94133
timestamp: DateTime::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc),
95134
complex_enum: ComplexEnum::VariantOne { int: 23 },
135+
untagged_enum_person: UntaggedEnumPerson::SexAndAge(SexAndAge {
136+
age: 42,
137+
sex: Sex::Male,
138+
})
96139
}))
97140
.unwrap(),
98141
serde_json::json!({
@@ -111,6 +154,10 @@ fn test_serialized_matches_expected() {
111154
"variantOne": {
112155
"int": 23
113156
}
157+
},
158+
"untaggedEnumPerson": {
159+
"age": 42,
160+
"sex": "Male"
114161
}
115162
}
116163
})
@@ -220,13 +267,42 @@ fn test_crd_schema_matches_expected() {
220267
"required": ["variantThree"]
221268
}
222269
],
223-
"description": "This is a complex enum"
270+
"description": "This is a complex enum with a description"
271+
},
272+
"untaggedEnumPerson": {
273+
"type": "object",
274+
"properties": {
275+
"age": {
276+
"type": "integer",
277+
"format": "int32",
278+
"description": "Age of the person in years"
279+
},
280+
"dateOfBirth": {
281+
"type": "string",
282+
"description": "Date of birth of the person as ISO 8601 date"
283+
},
284+
"sex": {
285+
"type": "string",
286+
"enum": ["Female", "Male", "Other"],
287+
"description": "Sex of the person"
288+
}
289+
},
290+
"anyOf": [
291+
{
292+
"required": ["age", "sex"]
293+
},
294+
{
295+
"required": ["dateOfBirth", "sex"]
296+
}
297+
],
298+
"description": "This is a untagged enum with a description"
224299
}
225300
},
226301
"required": [
227302
"complexEnum",
228303
"nonNullable",
229-
"timestamp"
304+
"timestamp",
305+
"untaggedEnumPerson"
230306
],
231307
"type": "object"
232308
}

0 commit comments

Comments
 (0)