Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for untagged enums in CRDs #1028

Merged
merged 13 commits into from Oct 7, 2022
81 changes: 65 additions & 16 deletions kube-core/src/schema.rs
Expand Up @@ -8,7 +8,7 @@ use std::collections::btree_map::Entry;
#[allow(unused_imports)] use schemars::gen::SchemaSettings;

use schemars::{
schema::{Metadata, ObjectValidation, Schema, SchemaObject},
schema::{InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SingleOrVec},
visit::Visitor,
};

Expand Down Expand Up @@ -78,21 +78,7 @@ impl Visitor for StructuralSchemaRewriter {
// Kubernetes doesn't allow variants to set additionalProperties
variant_obj.additional_properties = None;

// Try to merge metadata
match (&mut schema.instance_type, variant_type.take()) {
(_, None) => {}
(common_type @ None, variant_type) => {
*common_type = variant_type;
}
(Some(common_type), Some(variant_type)) => {
if *common_type != variant_type {
panic!(
"variant defined type {:?}, conflicting with existing type {:?}",
variant_type, common_type
);
}
}
}
merge_metadata(&mut schema.instance_type, variant_type.take());
}
}
}
Expand All @@ -108,6 +94,49 @@ impl Visitor for StructuralSchemaRewriter {
.insert("x-kubernetes-preserve-unknown-fields".into(), true.into());
}
}

// Support untagged enums
if let Some(any_of) = schema
.subschemas
.as_mut()
.and_then(|subschemas| subschemas.any_of.as_mut())
{
let common_obj = schema
.object
.get_or_insert_with(|| Box::new(ObjectValidation::default()));

for variant in any_of {
if let Schema::Object(SchemaObject {
instance_type: variant_type,
object: Some(variant_obj),
..
}) = variant
{
let variant_properties = std::mem::take(&mut variant_obj.properties);

for (property_name, property) in variant_properties {
match common_obj.properties.entry(property_name) {
Entry::Occupied(entry) => {
if &property != entry.get() {
panic!("Property {:?} has the schema {:?} but was already defined as {:?} in another untagged enum variant. The schemas for a property used in multiple untagged enum variants must be identical",
entry.key(),
&property,
entry.get());
}
}
Entry::Vacant(entry) => {
entry.insert(property);
}
}

// Kubernetes doesn't allow variants to set additionalProperties
variant_obj.additional_properties = None;

merge_metadata(&mut schema.instance_type, variant_type.take());
}
}
}
}
}
}

Expand All @@ -118,3 +147,23 @@ fn only_item<I: Iterator>(mut i: I) -> Option<I::Item> {
}
Some(item)
}

fn merge_metadata(
instance_type: &mut Option<SingleOrVec<InstanceType>>,
variant_type: Option<SingleOrVec<InstanceType>>,
) {
match (instance_type, variant_type) {
(_, None) => {}
(common_type @ None, variant_type) => {
*common_type = variant_type;
}
(Some(common_type), Some(variant_type)) => {
if *common_type != variant_type {
panic!(
"variant defined type {:?}, conflicting with existing type {:?}",
variant_type, common_type
);
}
}
}
}
82 changes: 79 additions & 3 deletions kube-derive/tests/crd_schema_test.rs
@@ -1,3 +1,5 @@
#![recursion_limit = "256"]

use chrono::{DateTime, NaiveDateTime, Utc};
use kube_derive::CustomResource;
use schemars::JsonSchema;
Expand Down Expand Up @@ -37,8 +39,11 @@ struct FooSpec {
// Using feature `chrono`
timestamp: DateTime<Utc>,

/// This is a complex enum
/// This is a complex enum with a description
complex_enum: ComplexEnum,

/// This is a untagged enum with a description
untagged_enum_person: UntaggedEnumPerson,
}

fn default_value() -> String {
Expand Down Expand Up @@ -69,6 +74,40 @@ enum ComplexEnum {
VariantThree {},
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
enum UntaggedEnumPerson {
SexAndAge(SexAndAge),
SexAndDateOfBirth(SexAndDateOfBirth),
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
struct SexAndAge {
/// Sex of the person
sex: Sex,
/// Age of the person in years
age: i32,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
struct SexAndDateOfBirth {
/// Sex of the person
sex: Sex,
/// Date of birth of the person as ISO 8601 date
date_of_birth: String,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
#[serde(rename_all = "PascalCase")]
enum Sex {
Female,
Male,
Other,
}

#[test]
fn test_crd_name() {
use kube::core::CustomResourceExt;
Expand All @@ -93,6 +132,10 @@ fn test_serialized_matches_expected() {
nullable_with_default: None,
timestamp: DateTime::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc),
complex_enum: ComplexEnum::VariantOne { int: 23 },
untagged_enum_person: UntaggedEnumPerson::SexAndAge(SexAndAge {
age: 42,
sex: Sex::Male,
})
}))
.unwrap(),
serde_json::json!({
Expand All @@ -111,6 +154,10 @@ fn test_serialized_matches_expected() {
"variantOne": {
"int": 23
}
},
"untaggedEnumPerson": {
"age": 42,
"sex": "Male"
}
}
})
Expand Down Expand Up @@ -220,13 +267,42 @@ fn test_crd_schema_matches_expected() {
"required": ["variantThree"]
}
],
"description": "This is a complex enum"
"description": "This is a complex enum with a description"
},
"untaggedEnumPerson": {
"type": "object",
"properties": {
"age": {
"type": "integer",
"format": "int32",
"description": "Age of the person in years"
},
"dateOfBirth": {
"type": "string",
"description": "Date of birth of the person as ISO 8601 date"
},
"sex": {
"type": "string",
"enum": ["Female", "Male", "Other"],
"description": "Sex of the person"
}
},
"anyOf": [
{
"required": ["age", "sex"]
},
{
"required": ["dateOfBirth", "sex"]
}
],
"description": "This is a untagged enum with a description"
}
},
"required": [
"complexEnum",
"nonNullable",
"timestamp"
"timestamp",
"untaggedEnumPerson"
],
"type": "object"
}
Expand Down