Skip to content

Commit

Permalink
Add support for untagged enums in CRDs
Browse files Browse the repository at this point in the history
Signed-off-by: Sebastian Bernauer <sebastian.bernauer@stackable.de>
  • Loading branch information
sbernauer committed Sep 26, 2022
1 parent 2821378 commit 440f60b
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 19 deletions.
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

0 comments on commit 440f60b

Please sign in to comment.