55// Used in docs
66#[ allow( unused_imports) ] use schemars:: generate:: SchemaSettings ;
77
8- use schemars:: { transform:: Transform , JsonSchema } ;
8+ use schemars:: {
9+ transform:: { transform_subschemas, Transform } ,
10+ JsonSchema ,
11+ } ;
912use serde:: { Deserialize , Serialize } ;
10- use serde_json:: Value ;
13+ use serde_json:: { json , Value } ;
1114use std:: collections:: { btree_map:: Entry , BTreeMap , BTreeSet } ;
1215
1316/// schemars [`Visitor`] that rewrites a [`Schema`] to conform to Kubernetes' "structural schema" rules
@@ -27,6 +30,42 @@ use std::collections::{btree_map::Entry, BTreeMap, BTreeSet};
2730#[ derive( Debug , Clone ) ]
2831pub struct StructuralSchemaRewriter ;
2932
33+ /// Recursively restructures JSON Schema objects so that the Option<Enum> object
34+ /// is returned per k8s CRD schema expectations.
35+ ///
36+ /// In kube 2.x the schema output behavior for `Option<Enum>` types changed.
37+ ///
38+ /// Previously given an enum like:
39+ ///
40+ /// ```rust
41+ /// enum LogLevel {
42+ /// Debug,
43+ /// Info,
44+ /// Error,
45+ /// }
46+ /// ```
47+ ///
48+ /// The following would be generated for Optional<LogLevel>:
49+ ///
50+ /// ```json
51+ /// { "enum": ["Debug", "Info", "Error"], "type": "string", "nullable": true }
52+ /// ```
53+ ///
54+ /// Now, schemars generates `anyOf` for `Option<LogLevel>` like:
55+ ///
56+ /// ```json
57+ /// {
58+ /// "anyOf": [
59+ /// { "enum": ["Debug", "Info", "Error"], "type": "string" },
60+ /// { "enum": [null], "nullable": true }
61+ /// ]
62+ /// }
63+ /// ```
64+ ///
65+ /// This transform implementation prevents this specific case from happening.
66+ #[ derive( Debug , Clone , Default ) ]
67+ pub struct OptionalEnum ;
68+
3069/// A JSON Schema.
3170#[ allow( clippy:: large_enum_variant) ]
3271#[ derive( Serialize , Deserialize , Debug , Clone , PartialEq , JsonSchema ) ]
@@ -305,6 +344,45 @@ impl Transform for StructuralSchemaRewriter {
305344 }
306345}
307346
347+ impl Transform for OptionalEnum {
348+ fn transform ( & mut self , schema : & mut schemars:: Schema ) {
349+ transform_subschemas ( self , schema) ;
350+
351+ let obj = match schema. as_object_mut ( ) . filter ( |o| o. len ( ) == 1 ) {
352+ Some ( obj) => obj,
353+ None => return ,
354+ } ;
355+
356+ let arr = obj
357+ . get ( "anyOf" )
358+ . iter ( )
359+ . flat_map ( |any_of| any_of. as_array ( ) )
360+ . last ( )
361+ . cloned ( )
362+ . unwrap_or_default ( ) ;
363+
364+ if arr. len ( ) != 2 {
365+ return ;
366+ }
367+
368+ let ( first, second) = match ( arr[ 0 ] . as_object ( ) , arr[ 1 ] . as_object ( ) ) {
369+ ( Some ( first) , Some ( second) ) => ( first, second) ,
370+ _ => return ,
371+ } ;
372+
373+ if first. contains_key ( "enum" )
374+ && second. contains_key ( "enum" )
375+ && second. get ( "enum" ) == Some ( & json ! ( [ null] ) )
376+ && !first. contains_key ( "nullable" )
377+ && second. contains_key ( "nullable" )
378+ {
379+ obj. remove ( "anyOf" ) ;
380+ obj. append ( & mut first. clone ( ) ) ;
381+ obj. insert ( "nullable" . to_string ( ) , Value :: Bool ( true ) ) ;
382+ }
383+ }
384+ }
385+
308386/// Bring all plain enum values up to the root schema,
309387/// since Kubernetes doesn't allow subschemas to define enum options.
310388///
0 commit comments