Skip to content

Commit 285ab64

Browse files
committed
adjust and document the hoist_one_of_enum function
Note: As described on the function, it is overly documented to express intent where it can be a little unclear. Ideally where would be some clearer official Kubernetes documentation on the differences between regular OpenAPI 3 schemas, and what Kubernetes accepts.
1 parent 85eaf36 commit 285ab64

File tree

1 file changed

+81
-28
lines changed

1 file changed

+81
-28
lines changed

kube-core/src/schema.rs

Lines changed: 81 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
1010
use serde_json::Value;
1111
use std::{
1212
collections::{btree_map::Entry, BTreeMap, BTreeSet},
13-
13+
ops::Deref as _,
1414
};
1515

1616
/// schemars [`Visitor`] that rewrites a [`Schema`] to conform to Kubernetes' "structural schema" rules
@@ -252,7 +252,7 @@ enum SingleOrVec<T> {
252252
#[cfg(test)]
253253
mod test {
254254
use assert_json_diff::assert_json_eq;
255-
use schemars::{json_schema, schema_for, JsonSchema};
255+
use schemars::{json_schema, schema_for, JsonSchema};
256256
use serde::{Deserialize, Serialize};
257257

258258
use super::*;
@@ -276,8 +276,6 @@ mod test {
276276
#[test]
277277
fn hoisting_a_schema() {
278278
let schemars_schema = schema_for!(NormalEnum);
279-
let mut kube_schema: crate::schema::Schema =
280-
schemars_schema_to_kube_schema(schemars_schema.clone()).unwrap();
281279

282280
assert_json_eq!(
283281
schemars_schema,
@@ -308,9 +306,14 @@ mod test {
308306
}
309307
)
310308
);
311-
hoist_one_of_enum(&mut kube_schema);
309+
310+
311+
let kube_schema: crate::schema::Schema =
312+
schemars_schema_to_kube_schema(schemars_schema.clone()).unwrap();
313+
314+
let hoisted_kube_schema = hoist_one_of_enum(kube_schema);
312315
assert_json_eq!(
313-
kube_schema,
316+
hoisted_kube_schema,
314317
json_schema!(
315318
{
316319
"$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -334,44 +337,94 @@ fn schemars_schema_to_kube_schema(incoming: schemars::Schema) -> Result<Schema,
334337
serde_json::from_value(incoming.to_value())
335338
}
336339

337-
#[cfg(test)]
338-
fn hoist_one_of_enum(schema: &mut Schema) {
339-
if let Schema::Object(SchemaObject {
340+
/// Hoist `oneOf` into top level `enum`.
341+
///
342+
/// This will move all `enum` variants and `const` values under `oneOf` into a single top level `enum` along with `type`.
343+
/// It will panic if there are anomalies, like differences in `type` values, or lack of `enum` or `const` fields in the `oneOf` entries.
344+
///
345+
/// Note: variant descriptions will be lost in the process, and the original `oneOf` will be erased.
346+
///
347+
// Note: This function is heavily documented to express intent. It is intended to help developers
348+
// make adjustments for future Schemars changes.
349+
fn hoist_one_of_enum(incoming: Schema) -> Schema {
350+
// Run some initial checks in case there is nothing to do
351+
let Schema::Object(SchemaObject {
340352
subschemas: Some(subschemas),
341-
instance_type: object_type,
342-
enum_values: object_ebum,
343353
..
344-
}) = schema && let SubschemaValidation {
354+
}) = &incoming
355+
else {
356+
return incoming;
357+
};
358+
359+
let SubschemaValidation {
345360
one_of: Some(one_of), ..
346-
} = &**subschemas
347-
&& !one_of.is_empty()
361+
} = subschemas.deref()
362+
else {
363+
return incoming;
364+
};
365+
366+
if one_of.is_empty() {
367+
return incoming;
368+
}
369+
370+
// At this point, we need to create a new Schema and hoist the `oneOf`
371+
// variants' `enum`/`const` values up into a parent `enum`.
372+
let mut new_schema = incoming.clone();
373+
if let Schema::Object(SchemaObject {
374+
subschemas: Some(new_subschemas),
375+
instance_type: new_instance_type,
376+
enum_values: new_enum_values,
377+
..
378+
}) = &mut new_schema
348379
{
380+
// For each `oneOf`, get the `type`.
381+
// Panic if it has no `type`, or if the entry is a boolean.
349382
let mut types = one_of.iter().map(|obj| match obj {
350-
Schema::Object(SchemaObject { instance_type: Some(type_), ..}) => type_,
383+
Schema::Object(SchemaObject {
384+
instance_type: Some(r#type),
385+
..
386+
}) => r#type,
387+
// TODO (@NickLarsenNZ): Is it correct that JSON Schema oneOf must have a type?
351388
Schema::Object(_) => panic!("oneOf variants need to define a type!: {obj:?}"),
352389
Schema::Bool(_) => panic!("oneOf variants can not be of type boolean"),
353390
});
354-
let enums = one_of.iter().flat_map(|obj| match obj {
355-
Schema::Object(SchemaObject {enum_values: Some(enum_), ..}) => enum_.clone(),
356-
Schema::Object(SchemaObject {other, ..})=> match other.get("const") {
357-
Some(const_) => vec![const_.clone()],
358-
None => panic!("oneOf variant did not provide \"enum\" or \"const\": {obj:?}"),
359-
},
360-
Schema::Bool(_) => panic!("oneOf variants can not be of type boolean"),
361-
});
362391

363-
let first_type = types
392+
// Get the first `type` value, then panic if any subsequent `type` values differ.
393+
let hoisted_instance_type = types
364394
.next()
365395
.expect("oneOf must have at least one variant - we already checked that");
366-
if types.any(|t| t != first_type) {
396+
// TODO (@NickLarsenNZ): Didn't sbernauer say that the types
397+
if types.any(|t| t != hoisted_instance_type) {
367398
panic!("All oneOf variants must have the same type");
368399
}
369400

370-
*object_type = Some(first_type.clone());
371-
*object_ebum = Some(enums.collect());
372-
subschemas.one_of = None;
401+
*new_instance_type = Some(hoisted_instance_type.clone());
402+
403+
// For each `oneOf` entry, iterate over the `enum` and `const` values.
404+
// Panic on an entry that doesn't contain an `enum` or `const`.
405+
let new_enums = one_of.iter().flat_map(|obj| match obj {
406+
Schema::Object(SchemaObject {
407+
enum_values: Some(r#enum),
408+
..
409+
}) => r#enum.clone(),
410+
// Warning: The `const` check below must come after the enum check above.
411+
// Otherwise it will panic on a valid entry with an `enum`.
412+
Schema::Object(SchemaObject { other, .. }) => match other.get("const") {
413+
Some(r#const) => vec![r#const.clone()],
414+
None => panic!("oneOf variant did not provide \"enum\" or \"const\": {obj:?}"),
415+
},
416+
Schema::Bool(_) => panic!("oneOf variants can not be of type boolean"),
417+
});
418+
419+
// Just in case there were existing enum values, add to them.
420+
// TODO (@NickLarsenNZ): Check if `oneOf` and `enum` are mutually exclusive for a valid spec.
421+
new_enum_values.get_or_insert_default().extend(new_enums);
373422

423+
// We can clear out the existing oneOf's, since they will be hoisted below.
424+
new_subschemas.one_of = None;
374425
}
426+
427+
new_schema
375428
}
376429

377430
impl Transform for StructuralSchemaRewriter {

0 commit comments

Comments
 (0)