k8s_openapi_codegen_common/
lib.rs

1#![warn(rust_2018_idioms)]
2#![deny(clippy::all, clippy::pedantic)]
3#![allow(
4    clippy::default_trait_access,
5    clippy::missing_errors_doc,
6    clippy::missing_panics_doc,
7    clippy::must_use_candidate,
8    clippy::too_many_arguments,
9    clippy::too_many_lines,
10)]
11
12//! This crate contains common code for the [`k8s-openapi` code generator](https://github.com/Arnavion/k8s-openapi/tree/master/k8s-openapi-codegen)
13//! and the [`k8s-openapi-derive`](https://crates.io/crates/k8s-openapi-derive) custom derive crate.
14//!
15//! It can be used by code generators that want to generate crates like `k8s-openapi` and `k8s-openapi-derive` for Kubernetes-like software
16//! such as OpenShift.
17//!
18//! 1. Create a [`swagger20::Spec`] value, either by deserializing it from an OpenAPI spec JSON file or by creating it manually.
19//! 1. Invoke the [`run`] function for each definition in the spec.
20
21pub mod swagger20;
22
23mod templates;
24
25/// Statistics from a successful invocation of [`run`]
26#[derive(Clone, Copy, Debug)]
27pub struct RunResult {
28    pub num_generated_structs: usize,
29    pub num_generated_type_aliases: usize,
30}
31
32/// Error type reported by [`run`]
33#[derive(Debug)]
34pub struct Error(Box<dyn std::error::Error + Send + Sync>);
35
36impl std::fmt::Display for Error {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        self.0.fmt(f)
39    }
40}
41
42impl std::error::Error for Error {
43    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
44        self.0.source()
45    }
46}
47
48macro_rules! impl_from_for_error {
49    ($($ty:ty ,)*) => {
50        $(
51            impl From<$ty> for Error {
52                fn from(err: $ty) -> Self {
53                    Error(err.into())
54                }
55            }
56        )*
57    };
58}
59
60impl_from_for_error! {
61    &'_ str,
62    String,
63    std::fmt::Error,
64    std::io::Error,
65}
66
67/// A mechanism for converting (the components of) an openapi path to (the components of) a Rust namespace.
68///
69/// The k8s-openapi code generator uses this trait to emit the paths of all types relative to the `k8s_openapi` crate.
70/// For example, it maps the components of the openapi path `io.k8s.api.core.v1` to
71/// the components of the Rust namespace `crate::core::v1`. The `io.k8s.` prefix is stripped and the path starts with `crate::`.
72///
73/// Other code generators can have more complicated implementations. For example, an OpenShift code generator that has its own types but also wants to
74/// reuse types from the k8s-openapi crate would map `com.github.openshift.` to `crate::` and `io.k8s.` to `k8s_openapi::` instead.
75///
76/// The implementation should return `None` for paths that it does not recognize.
77pub trait MapNamespace {
78    fn map_namespace<'a>(&self, path_parts: &[&'a str]) -> Option<Vec<&'a str>>;
79}
80
81/// Used to create an impl of `std::io::Write` for each type that the type's generated code will be written to.
82pub trait RunState {
83    /// The impl of `std::io::Write` for each type that the type's generated code will be written to.
84    type Writer: std::io::Write;
85
86    /// Returns an impl of `std::io::Write` for each type that the type's generated code will be written to.
87    ///
88    /// # Parameters
89    ///
90    /// - `parts`: A list of strings making up the components of the path of the generated type. Code generators that are emitting crates
91    ///   can use this parameter to make module subdirectories for each component, and to emit `use` statements in the final module's `mod.rs`.
92    fn make_writer(&mut self, parts: &[&str]) -> std::io::Result<Self::Writer>;
93
94    /// This function is invoked when `k8s_openapi_codegen_common::run` is done with the writer and completes successfully.
95    /// The implementation can do any cleanup that it wants here.
96    fn finish(&mut self, writer: Self::Writer);
97}
98
99impl<T> RunState for &'_ mut T where T: RunState {
100    type Writer = <T as RunState>::Writer;
101
102    fn make_writer(&mut self, parts: &[&str]) -> std::io::Result<Self::Writer> {
103        (*self).make_writer(parts)
104    }
105
106    fn finish(&mut self, writer: Self::Writer) {
107        (*self).finish(writer);
108    }
109}
110
111/// Whether [`run`] should generate an impl of `schemars::JsonSchema` for the type or not.
112#[derive(Clone, Copy, Debug)]
113pub enum GenerateSchema<'a> {
114    Yes {
115        /// An optional feature that the impl of `schemars::JsonSchema` will be `cfg`-gated by.
116        feature: Option<&'a str>,
117    },
118
119    No,
120}
121
122/// Each invocation of this function generates a single type specified by the `definition_path` parameter.
123///
124/// # Parameters
125///
126/// - `definitions`: The definitions parsed from the OpenAPI spec that should be emitted as model types.
127///
128/// - `operations`: The list of operations parsed from the OpenAPI spec. The list is mutated to remove the operations
129///   that are determined to be associated with the type currently being generated.
130///
131/// - `definition_path`: The specific definition path out of the `definitions` collection that should be emitted.
132///
133/// - `map_namespace`: An instance of the [`MapNamespace`] trait that controls how OpenAPI namespaces of the definitions are mapped to rust namespaces.
134///
135/// - `vis`: The visibility modifier that should be emitted on the generated code.
136///
137/// - `state`: See the documentation of the [`RunState`] trait.
138pub fn run(
139    definitions: &std::collections::BTreeMap<swagger20::DefinitionPath, swagger20::Schema>,
140    operations: &mut Vec<swagger20::Operation>,
141    definition_path: &swagger20::DefinitionPath,
142    map_namespace: &impl MapNamespace,
143    vis: &str,
144    generate_schema: GenerateSchema<'_>,
145    mut state: impl RunState,
146) -> Result<RunResult, Error> {
147    let definition = definitions.get(definition_path).ok_or_else(|| format!("definition for {definition_path} does not exist in spec"))?;
148
149    let local = map_namespace_local_to_string(map_namespace)?;
150
151    let mut run_result = RunResult {
152        num_generated_structs: 0,
153        num_generated_type_aliases: 0,
154    };
155
156    let path_parts: Vec<_> = definition_path.split('.').collect();
157    let namespace_parts: Vec<_> =
158        map_namespace.map_namespace(&path_parts).ok_or_else(|| format!("unexpected path {definition_path:?}"))?
159        .into_iter()
160        .collect();
161
162    let mut out = state.make_writer(&namespace_parts)?;
163
164    let type_name = path_parts.last().ok_or_else(|| format!("path for {definition_path} has no parts"))?;
165
166    let derives = get_derives(&definition.kind, definitions, map_namespace)?;
167
168    templates::type_header::generate(
169        &mut out,
170        definition_path,
171        definition.description.as_deref(),
172        derives,
173        vis,
174    )?;
175
176    match &definition.kind {
177        swagger20::SchemaKind::Properties(properties) => {
178            let (template_properties, resource_metadata, metadata_ty) = {
179                let mut result = Vec::with_capacity(properties.len());
180
181                let mut single_group_version_kind = match &definition.kubernetes_group_kind_versions[..] {
182                    [group_version_kind] => Some((group_version_kind, false, false)),
183                    _ => None,
184                };
185
186                let mut metadata_ty = None;
187
188                for (name, (schema, required)) in properties {
189                    if name.0 == "apiVersion" {
190                        if let Some((_, has_api_version, _)) = &mut single_group_version_kind {
191                            *has_api_version = true;
192                            continue;
193                        }
194                    }
195
196                    if name.0 == "kind" {
197                        if let Some((_, _, has_kind)) = &mut single_group_version_kind {
198                            *has_kind = true;
199                            continue;
200                        }
201                    }
202
203                    let field_name = get_rust_ident(name);
204
205                    let mut field_type_name = String::new();
206
207                    let required = match required {
208                        true => templates::PropertyRequired::Required {
209                            is_default: is_default(&schema.kind, definitions, map_namespace)?,
210                        },
211                        false => templates::PropertyRequired::Optional,
212                    };
213
214                    if let templates::PropertyRequired::Optional = required {
215                        field_type_name.push_str("Option<");
216                    }
217
218                    let type_name = get_rust_type(&schema.kind, map_namespace)?;
219
220                    if name.0 == "metadata" {
221                        metadata_ty = Some((type_name.clone(), required));
222                    }
223
224                    // Fix cases of infinite recursion
225                    if let swagger20::SchemaKind::Ref(swagger20::RefPath { path, .. }) = &schema.kind {
226                        match (&**definition_path, &**name, &**path) {
227                            (
228                                "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps",
229                                "not",
230                                "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps",
231                            ) |
232                            (
233                                "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps",
234                                "not",
235                                "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps",
236                            ) => {
237                                field_type_name.push_str("std::boxed::Box<");
238                                field_type_name.push_str(&type_name);
239                                field_type_name.push('>');
240                            },
241
242                            _ => field_type_name.push_str(&type_name),
243                        }
244                    }
245                    else {
246                        field_type_name.push_str(&type_name);
247                    }
248
249                    if let templates::PropertyRequired::Optional = required {
250                        field_type_name.push('>');
251                    }
252
253                    let is_flattened = matches!(&schema.kind, swagger20::SchemaKind::Ty(swagger20::Type::CustomResourceSubresources(_)));
254
255                    result.push(templates::Property {
256                        name,
257                        comment: schema.description.as_deref(),
258                        field_name,
259                        field_type_name,
260                        required,
261                        is_flattened,
262                        merge_type: &schema.merge_type,
263                    });
264                }
265
266                let resource_metadata = match single_group_version_kind {
267                    Some((single_group_version_kind, true, true)) =>
268                        Some(if single_group_version_kind.group.is_empty() {
269                            (
270                                format!("{:?}", single_group_version_kind.version),
271                                format!("{:?}", ""),
272                                format!("{:?}", single_group_version_kind.kind),
273                                format!("{:?}", single_group_version_kind.version),
274                                definition.list_kind.as_ref().map(|kind| format!("{kind:?}")),
275                            )
276                        }
277                        else {
278                            (
279                                format!("{:?}", format!("{}/{}", single_group_version_kind.group, single_group_version_kind.version)),
280                                format!("{:?}", single_group_version_kind.group),
281                                format!("{:?}", single_group_version_kind.kind),
282                                format!("{:?}", single_group_version_kind.version),
283                                definition.list_kind.as_ref().map(|kind| format!("{kind:?}")),
284                            )
285                        }),
286                    Some((_, true, false)) => return Err(format!("{definition_path} has an apiVersion property but not a kind property").into()),
287                    Some((_, false, true)) => return Err(format!("{definition_path} has a kind property but not an apiVersion property").into()),
288                    Some((_, false, false)) | None => None,
289                };
290
291                (result, resource_metadata, metadata_ty)
292            };
293
294            templates::r#struct::generate(
295                &mut out,
296                vis,
297                type_name,
298                Default::default(),
299                &template_properties,
300            )?;
301
302            let mut namespace_or_cluster_scoped_url_path_segment_and_scope = vec![];
303            let mut subresource_url_path_segment_and_scope = vec![];
304
305            if !definition.kubernetes_group_kind_versions.is_empty() {
306                let mut kubernetes_group_kind_versions: Vec<_> = definition.kubernetes_group_kind_versions.iter().collect();
307                kubernetes_group_kind_versions.sort();
308
309                let mut operations_by_gkv: std::collections::BTreeMap<_, Vec<_>> = Default::default();
310                for operation in std::mem::take(operations) {
311                    operations_by_gkv
312                        .entry(operation.kubernetes_group_kind_version.clone())
313                        .or_default()
314                        .push(operation);
315                }
316
317                for kubernetes_group_kind_version in kubernetes_group_kind_versions {
318                    if let Some(mut operations) = operations_by_gkv.remove(&Some(kubernetes_group_kind_version.clone())) {
319                        operations.sort_by(|o1, o2| o1.id.cmp(&o2.id));
320
321                        for operation in operations {
322                            // If this is a CRUD operation, use it to determine the resource's URL path segment and scope.
323                            match operation.kubernetes_action {
324                                Some(
325                                    swagger20::KubernetesAction::Delete |
326                                    swagger20::KubernetesAction::Get |
327                                    swagger20::KubernetesAction::Post |
328                                    swagger20::KubernetesAction::Put
329                                ) => (),
330                                _ => continue,
331                            }
332                            let mut components = operation.path.rsplit('/');
333                            let components = (
334                                components.next().expect("str::rsplit returns at least one component"),
335                                components.next(),
336                                components.next(),
337                                components.next(),
338                            );
339
340                            let (url_path_segment_, scope_, url_path_segment_and_scope) = match components {
341                                ("{name}", Some(url_path_segment), Some("{namespace}"), Some("namespaces")) =>
342                                    (
343                                        format!("{url_path_segment:?}"),
344                                        format!("{local}NamespaceResourceScope"),
345                                        &mut namespace_or_cluster_scoped_url_path_segment_and_scope,
346                                    ),
347
348                                ("{name}", Some(url_path_segment), _, _) =>
349                                    (
350                                        format!("{url_path_segment:?}"),
351                                        format!("{local}ClusterResourceScope"),
352                                        &mut namespace_or_cluster_scoped_url_path_segment_and_scope,
353                                    ),
354
355                                (url_path_segment, Some("{name}"), _, _) =>
356                                    (
357                                        format!("{url_path_segment:?}"),
358                                        format!("{local}SubResourceScope"),
359                                        &mut subresource_url_path_segment_and_scope,
360                                    ),
361
362                                (url_path_segment, Some("{namespace}"), Some("namespaces"), _) =>
363                                    (
364                                        format!("{url_path_segment:?}"),
365                                        format!("{local}NamespaceResourceScope"),
366                                        &mut namespace_or_cluster_scoped_url_path_segment_and_scope,
367                                    ),
368
369                                (url_path_segment, _, _, _) =>
370                                    (
371                                        format!("{url_path_segment:?}"),
372                                        format!("{local}ClusterResourceScope"),
373                                        &mut namespace_or_cluster_scoped_url_path_segment_and_scope,
374                                    ),
375                            };
376
377                            url_path_segment_and_scope.push((url_path_segment_, scope_));
378                        }
379                    }
380                }
381
382                *operations = operations_by_gkv.into_values().flatten().collect();
383            }
384
385            match &**definition_path {
386                "io.k8s.apimachinery.pkg.apis.meta.v1.APIGroup" |
387                "io.k8s.apimachinery.pkg.apis.meta.v1.APIGroupList" |
388                "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" |
389                "io.k8s.apimachinery.pkg.apis.meta.v1.APIVersions" =>
390                    namespace_or_cluster_scoped_url_path_segment_and_scope.push((r#""""#.to_owned(), format!("{local}ClusterResourceScope"))),
391                "io.k8s.apimachinery.pkg.apis.meta.v1.Status" =>
392                    subresource_url_path_segment_and_scope.push((r#""status""#.to_owned(), format!("{local}SubResourceScope"))),
393                _ => (),
394            }
395
396            namespace_or_cluster_scoped_url_path_segment_and_scope.dedup();
397            subresource_url_path_segment_and_scope.dedup();
398
399            let template_resource_metadata = match (&resource_metadata, &metadata_ty) {
400                (
401                    Some((api_version, group, kind, version, list_kind)),
402                    Some((metadata_ty, templates::PropertyRequired::Required { is_default: _ })),
403                ) => Some(templates::ResourceMetadata {
404                    api_version,
405                    group,
406                    kind,
407                    version,
408                    list_kind: list_kind.as_deref(),
409                    metadata_ty: Some(metadata_ty),
410                    url_path_segment_and_scope: match (&*namespace_or_cluster_scoped_url_path_segment_and_scope, &*subresource_url_path_segment_and_scope) {
411                        ([(url_path_segment, scope)], _) |
412                        ([], [(url_path_segment, scope)]) => (&**url_path_segment, &**scope),
413
414                        ([], []) => return Err(format!(
415                            "definition {definition_path} is a Resource but its URL path segment and scope could not be inferred").into()),
416                        ([_, ..], _) => return Err(format!(
417                            "definition {definition_path} is a Resource but was inferred to have multiple scopes {namespace_or_cluster_scoped_url_path_segment_and_scope:?}").into()),
418                        ([], [_, ..]) => return Err(format!(
419                            "definition {definition_path} is a Resource but was inferred to have multiple scopes {subresource_url_path_segment_and_scope:?}").into()),
420                    },
421                }),
422
423                (Some(_), Some((_, templates::PropertyRequired::Optional | templates::PropertyRequired::OptionalDefault))) =>
424                    return Err(format!("definition {definition_path} has optional metadata").into()),
425
426                (
427                    Some((api_version, group, kind, version, list_kind)),
428                    None,
429                ) => Some(templates::ResourceMetadata {
430                    api_version,
431                    group,
432                    kind,
433                    version,
434                    list_kind: list_kind.as_deref(),
435                    metadata_ty: None,
436                    url_path_segment_and_scope: match (&*namespace_or_cluster_scoped_url_path_segment_and_scope, &*subresource_url_path_segment_and_scope) {
437                        ([(url_path_segment, scope)], _) |
438                        ([], [(url_path_segment, scope)]) => (&**url_path_segment, &**scope),
439
440                        ([], []) => return Err(format!(
441                            "definition {definition_path} is a Resource but its URL path segment and scope could not be inferred").into()),
442                        ([_, _, ..], _) => return Err(format!(
443                            "definition {definition_path} is a Resource but was inferred to have multiple scopes {namespace_or_cluster_scoped_url_path_segment_and_scope:?}").into()),
444                        ([], [_, _, ..]) => return Err(format!(
445                            "definition {definition_path} is a Resource but was inferred to have multiple scopes {subresource_url_path_segment_and_scope:?}").into()),
446                    },
447                }),
448
449                (None, _) => None,
450            };
451
452            if let Some(template_resource_metadata) = &template_resource_metadata {
453                templates::impl_resource::generate(
454                    &mut out,
455                    type_name,
456                    Default::default(),
457                    map_namespace,
458                    template_resource_metadata,
459                )?;
460
461                templates::impl_listable_resource::generate(
462                    &mut out,
463                    type_name,
464                    Default::default(),
465                    map_namespace,
466                    template_resource_metadata,
467                )?;
468
469                templates::impl_metadata::generate(
470                    &mut out,
471                    type_name,
472                    Default::default(),
473                    map_namespace,
474                    template_resource_metadata,
475                )?;
476            }
477
478            if definition.impl_deep_merge {
479                templates::struct_deep_merge::generate(
480                    &mut out,
481                    type_name,
482                    Default::default(),
483                    &template_properties,
484                    map_namespace,
485                )?;
486            }
487
488            templates::impl_deserialize::generate(
489                &mut out,
490                type_name,
491                Default::default(),
492                &template_properties,
493                map_namespace,
494                template_resource_metadata.as_ref(),
495            )?;
496
497            templates::impl_serialize::generate(
498                &mut out,
499                type_name,
500                Default::default(),
501                &template_properties,
502                map_namespace,
503                template_resource_metadata.as_ref(),
504            )?;
505
506            run_result.num_generated_structs += 1;
507        },
508
509        swagger20::SchemaKind::Ref(_) => return Err(format!("{definition_path} is a Ref").into()),
510
511        swagger20::SchemaKind::Ty(swagger20::Type::IntOrString) => {
512            templates::int_or_string::generate(
513                &mut out,
514                type_name,
515                map_namespace,
516            )?;
517
518            run_result.num_generated_structs += 1;
519        },
520
521        swagger20::SchemaKind::Ty(swagger20::Type::JsonSchemaPropsOr(namespace, json_schema_props_or)) => {
522            let json_schema_props_or = match json_schema_props_or {
523                swagger20::JsonSchemaPropsOr::Array => templates::json_schema_props_or::Or::Array,
524                swagger20::JsonSchemaPropsOr::Bool => templates::json_schema_props_or::Or::Bool,
525                swagger20::JsonSchemaPropsOr::StringArray => templates::json_schema_props_or::Or::StringArray,
526            };
527
528            let json_schema_props_type_name =
529                get_fully_qualified_type_name(
530                    &swagger20::RefPath {
531                        path: format!("io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.{namespace}.JSONSchemaProps"),
532                        can_be_default: None,
533                    },
534                    map_namespace);
535
536            templates::json_schema_props_or::generate(
537                &mut out,
538                type_name,
539                json_schema_props_or,
540                &json_schema_props_type_name,
541                map_namespace,
542            )?;
543
544            run_result.num_generated_structs += 1;
545        },
546
547        swagger20::SchemaKind::Ty(swagger20::Type::Quantity) => {
548            templates::quantity::generate(
549                &mut out,
550                type_name,
551                map_namespace,
552            )?;
553
554            run_result.num_generated_structs += 1;
555        },
556
557        swagger20::SchemaKind::Ty(swagger20::Type::Patch) => {
558            templates::patch::generate(
559                &mut out,
560                type_name,
561                map_namespace,
562            )?;
563
564            run_result.num_generated_structs += 1;
565        },
566
567        swagger20::SchemaKind::Ty(swagger20::Type::WatchEvent(raw_extension_ref_path)) => {
568            let error_status_rust_type = get_rust_type(
569                &swagger20::SchemaKind::Ref(swagger20::RefPath {
570                    path: "io.k8s.apimachinery.pkg.apis.meta.v1.Status".to_owned(),
571                    can_be_default: None,
572                }),
573                map_namespace,
574            )?;
575
576            let error_other_rust_type = get_rust_type(
577                &swagger20::SchemaKind::Ref(raw_extension_ref_path.clone()),
578                map_namespace,
579            )?;
580
581            templates::watch_event::generate(
582                &mut out,
583                type_name,
584                &error_status_rust_type,
585                &error_other_rust_type,
586                map_namespace,
587            )?;
588
589            run_result.num_generated_structs += 1;
590        },
591
592        swagger20::SchemaKind::Ty(swagger20::Type::ListDef { metadata }) => {
593            let metadata_rust_type = get_rust_type(metadata, map_namespace)?;
594
595            let template_generics_where_part = format!("T: {local}ListableResource");
596            let template_generics = templates::Generics {
597                type_part: Some("T"),
598                where_part: Some(&template_generics_where_part),
599            };
600
601            let items_merge_type = swagger20::MergeType::List {
602                strategy: swagger20::KubernetesListType::Map,
603                keys: vec!["metadata().namespace".to_string(), "metadata().name".to_string()],
604                item_merge_type: Box::new(swagger20::MergeType::Default),
605            };
606
607            let template_properties = vec![
608                templates::Property {
609                    name: "items",
610                    comment: Some("List of objects."),
611                    field_name: "items".into(),
612                    field_type_name: "std::vec::Vec<T>".to_owned(),
613                    required: templates::PropertyRequired::Required { is_default: true },
614                    is_flattened: false,
615                    merge_type: &items_merge_type,
616                },
617
618                templates::Property {
619                    name: "metadata",
620                    comment: Some("Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds"),
621                    field_name: "metadata".into(),
622                    field_type_name: (*metadata_rust_type).to_owned(),
623                    required: templates::PropertyRequired::Required { is_default: true },
624                    is_flattened: false,
625                    merge_type: &swagger20::MergeType::Default,
626                },
627            ];
628
629            let template_resource_metadata = templates::ResourceMetadata {
630                api_version: "<T as crate::Resource>::API_VERSION",
631                group: "<T as crate::Resource>::GROUP",
632                kind: "<T as crate::ListableResource>::LIST_KIND",
633                version: "<T as crate::Resource>::VERSION",
634                list_kind: None,
635                metadata_ty: Some(&metadata_rust_type),
636                url_path_segment_and_scope: (r#""""#, "<T as crate::Resource>::Scope"),
637            };
638
639            templates::r#struct::generate(
640                &mut out,
641                vis,
642                type_name,
643                template_generics,
644                &template_properties,
645            )?;
646
647            templates::impl_resource::generate(
648                &mut out,
649                type_name,
650                template_generics,
651                map_namespace,
652                &template_resource_metadata,
653            )?;
654
655            templates::impl_listable_resource::generate(
656                &mut out,
657                type_name,
658                template_generics,
659                map_namespace,
660                &template_resource_metadata,
661            )?;
662
663            templates::impl_metadata::generate(
664                &mut out,
665                type_name,
666                template_generics,
667                map_namespace,
668                &template_resource_metadata,
669            )?;
670
671            if definition.impl_deep_merge {
672                let template_generics_where_part = format!("T: {local}DeepMerge + {local}Metadata<Ty = {local}apimachinery::pkg::apis::meta::v1::ObjectMeta> + {local}ListableResource");
673                let template_generics = templates::Generics {
674                    where_part: Some(&template_generics_where_part),
675                    ..template_generics
676                };
677
678                templates::struct_deep_merge::generate(
679                    &mut out,
680                    type_name,
681                    template_generics,
682                    &template_properties,
683                    map_namespace,
684                )?;
685            }
686
687            {
688                let template_generics_where_part = format!("T: {local}serde::Deserialize<'de> + {local}ListableResource");
689                let template_generics = templates::Generics {
690                    where_part: Some(&template_generics_where_part),
691                    ..template_generics
692                };
693
694                templates::impl_deserialize::generate(
695                    &mut out,
696                    type_name,
697                    template_generics,
698                    &template_properties,
699                    map_namespace,
700                    Some(&template_resource_metadata),
701                )?;
702            }
703
704            {
705                let template_generics_where_part = format!("T: {local}serde::Serialize + {local}ListableResource");
706                let template_generics = templates::Generics {
707                    where_part: Some(&template_generics_where_part),
708                    ..template_generics
709                };
710
711                templates::impl_serialize::generate(
712                    &mut out,
713                    type_name,
714                    template_generics,
715                    &template_properties,
716                    map_namespace,
717                    Some(&template_resource_metadata),
718                )?;
719            }
720
721            run_result.num_generated_structs += 1;
722        },
723
724        swagger20::SchemaKind::Ty(swagger20::Type::ListRef { .. }) => return Err(format!("definition {definition_path} is a ListRef").into()),
725
726        swagger20::SchemaKind::Ty(_) => {
727            let inner_type_name = get_rust_type(&definition.kind, map_namespace)?;
728
729            // Kubernetes requires MicroTime to be serialized with exactly six decimal digits, instead of the default serde serialization of `chrono::DateTime`
730            // that uses a variable number up to nine.
731            //
732            // Furthermore, while Kubernetes does deserialize a Time from a string with one or more decimal digits,
733            // the format string it uses to *serialize* datetimes does not contain any decimal digits. So match that behavior just to be safe, and to have
734            // the same behavior as the golang client.
735            //
736            // Refs:
737            // - https://github.com/Arnavion/k8s-openapi/issues/63
738            // - https://github.com/deislabs/krustlet/issues/5
739            // - https://github.com/kubernetes/apimachinery/issues/88
740            let datetime_serialization_format = match (&**definition_path, &definition.kind) {
741                (
742                    "io.k8s.apimachinery.pkg.apis.meta.v1.MicroTime",
743                    swagger20::SchemaKind::Ty(swagger20::Type::String { format: Some(swagger20::StringFormat::DateTime) }),
744                ) => templates::DateTimeSerializationFormat::SixDecimalDigits,
745
746                (
747                    "io.k8s.apimachinery.pkg.apis.meta.v1.Time",
748                    swagger20::SchemaKind::Ty(swagger20::Type::String { format: Some(swagger20::StringFormat::DateTime) }),
749                ) => templates::DateTimeSerializationFormat::ZeroDecimalDigits,
750
751                _ => templates::DateTimeSerializationFormat::Default,
752            };
753
754            templates::newtype::generate(
755                &mut out,
756                vis,
757                type_name,
758                &inner_type_name,
759                datetime_serialization_format,
760                map_namespace,
761            )?;
762
763            run_result.num_generated_type_aliases += 1;
764        },
765    }
766
767    if let GenerateSchema::Yes { feature: schema_feature } = generate_schema {
768        match &definition.kind {
769            swagger20::SchemaKind::Properties(_) |
770            swagger20::SchemaKind::Ty(
771                swagger20::Type::Any |
772                swagger20::Type::Array { .. } |
773                swagger20::Type::Boolean |
774                swagger20::Type::Integer { .. } |
775                swagger20::Type::Number { .. } |
776                swagger20::Type::Object { .. } |
777                swagger20::Type::String { .. } |
778                swagger20::Type::IntOrString |
779                swagger20::Type::JsonSchemaPropsOr(_, _) |
780                swagger20::Type::Quantity |
781                swagger20::Type::Patch
782            ) => {
783                templates::impl_schema::generate(
784                    &mut out,
785                    type_name,
786                    Default::default(),
787                    definition_path,
788                    definition,
789                    schema_feature,
790                    map_namespace,
791                )?;
792            }
793
794            swagger20::SchemaKind::Ty(swagger20::Type::WatchEvent(_)) => {
795                templates::impl_schema::generate(
796                    &mut out,
797                    type_name,
798                    templates::Generics {
799                        type_part: Some("T"),
800                        where_part: None,
801                    },
802                    definition_path,
803                    definition,
804                    schema_feature,
805                    map_namespace,
806                )?;
807            }
808
809            _ => (),
810        }
811    }
812
813    state.finish(out);
814
815    Ok(run_result)
816}
817
818fn map_namespace_local_to_string(map_namespace: &impl MapNamespace) -> Result<String, Error> {
819    let namespace_parts = map_namespace.map_namespace(&["io", "k8s"]).ok_or(r#"unexpected path "io.k8s""#)?;
820
821    let mut result = String::new();
822    for namespace_part in namespace_parts {
823        result.push_str(&get_rust_ident(namespace_part));
824        result.push_str("::");
825    }
826    Ok(result)
827}
828
829fn get_derives(
830    kind: &swagger20::SchemaKind,
831    definitions: &std::collections::BTreeMap<swagger20::DefinitionPath, swagger20::Schema>,
832    map_namespace: &impl MapNamespace,
833) -> Result<Option<templates::type_header::Derives>, Error> {
834    if matches!(kind, swagger20::SchemaKind::Ty(swagger20::Type::ListRef { .. })) {
835        // ListRef is emitted as a type alias.
836        return Ok(None);
837    }
838
839    let derive_clone =
840        evaluate_trait_bound(
841            kind,
842            true,
843            definitions,
844            map_namespace,
845            |_, _| Ok(true))?;
846
847    let derive_copy =
848        derive_clone &&
849        evaluate_trait_bound(
850            kind,
851            false,
852            definitions,
853            map_namespace,
854            |_, _| Ok(false))?;
855
856    #[allow(clippy::match_same_arms)]
857    let is_default = evaluate_trait_bound(kind, false, definitions, map_namespace, |kind, required| match kind {
858        // Option<T>::default is None regardless of T
859        _ if !required => Ok(true),
860
861        swagger20::SchemaKind::Ref(swagger20::RefPath { can_be_default: Some(can_be_default), .. }) => Ok(*can_be_default),
862
863        swagger20::SchemaKind::Ref(ref_path @ swagger20::RefPath { .. }) if ref_path.references_scope(map_namespace) => Ok(false),
864
865        // metadata field in resource type created by #[derive(CustomResourceDefinition)]
866        swagger20::SchemaKind::Ref(ref_path @ swagger20::RefPath { .. })
867            if !ref_path.references_scope(map_namespace) && ref_path.path == "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" => Ok(true),
868
869        // Handled by evaluate_trait_bound
870        swagger20::SchemaKind::Ref(ref_path @ swagger20::RefPath { .. }) if !ref_path.references_scope(map_namespace) => unreachable!(),
871
872        // chrono::DateTime<chrono::Utc> is not Default
873        swagger20::SchemaKind::Ty(swagger20::Type::String { format: Some(swagger20::StringFormat::DateTime) }) => Ok(false),
874
875        // Enums without a default value
876        swagger20::SchemaKind::Ty(
877            swagger20::Type::JsonSchemaPropsOr(_, _) |
878            swagger20::Type::Patch |
879            swagger20::Type::WatchEvent(_)
880        ) => Ok(false),
881
882        _ => Ok(true),
883    })?;
884    let derive_default =
885        is_default &&
886        // IntOrString has a manual Default impl, so don't #[derive] it.
887        !matches!(kind, swagger20::SchemaKind::Ty(swagger20::Type::IntOrString));
888
889    let derive_partial_eq =
890        evaluate_trait_bound(
891            kind,
892            true,
893            definitions,
894            map_namespace,
895            |_, _| Ok(true))?;
896
897    // The choice of deriving Eq, Ord and PartialOrd is deliberately more conservative than the choice of deriving PartialEq,
898    // so as to not change dramatically between Kubernetes versions. For example, ObjectMeta is Ord in v1.15 but not in v1.16 because
899    // it indirectly gained a serde_json::Value field (`managed_fields.fields_v1.0`).
900    //
901    // Also, being conservative means the types generated by #[derive(k8s_openapi_derive::CustomResource)] don't have to require them either.
902
903    let derive_eq =
904        derive_partial_eq &&
905        matches!(kind, swagger20::SchemaKind::Ty(
906            swagger20::Type::IntOrString |
907            swagger20::Type::String { format: Some(swagger20::StringFormat::DateTime) }
908        ));
909
910    let derive_partial_ord =
911        derive_partial_eq &&
912        matches!(kind, swagger20::SchemaKind::Ty(swagger20::Type::String { format: Some(swagger20::StringFormat::DateTime) }));
913
914    let derive_ord = derive_partial_ord && derive_eq;
915
916    Ok(Some(templates::type_header::Derives {
917        clone: derive_clone,
918        copy: derive_copy,
919        default: derive_default,
920        eq: derive_eq,
921        ord: derive_ord,
922        partial_eq: derive_partial_eq,
923        partial_ord: derive_partial_ord,
924    }))
925}
926
927fn is_default(
928    kind: &swagger20::SchemaKind,
929    definitions: &std::collections::BTreeMap<swagger20::DefinitionPath, swagger20::Schema>,
930    map_namespace: &impl MapNamespace,
931) -> Result<bool, Error> {
932    #[allow(clippy::match_same_arms)]
933    evaluate_trait_bound(kind, false, definitions, map_namespace, |kind, required| match kind {
934        // Option<T>::default is None regardless of T
935        _ if !required => Ok(true),
936
937        swagger20::SchemaKind::Ref(swagger20::RefPath { can_be_default: Some(can_be_default), .. }) => Ok(*can_be_default),
938
939        swagger20::SchemaKind::Ref(ref_path @ swagger20::RefPath { .. }) if ref_path.references_scope(map_namespace) => Ok(false),
940
941        // metadata field in resource type created by #[derive(CustomResourceDefinition)]
942        swagger20::SchemaKind::Ref(ref_path @ swagger20::RefPath { .. })
943            if !ref_path.references_scope(map_namespace) && ref_path.path == "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" => Ok(true),
944
945        // Handled by evaluate_trait_bound
946        swagger20::SchemaKind::Ref(ref_path @ swagger20::RefPath { .. }) if !ref_path.references_scope(map_namespace) => unreachable!(),
947
948        // chrono::DateTime<chrono::Utc> is not Default
949        swagger20::SchemaKind::Ty(swagger20::Type::String { format: Some(swagger20::StringFormat::DateTime) }) => Ok(false),
950
951        // Enums without a default value
952        swagger20::SchemaKind::Ty(
953            swagger20::Type::JsonSchemaPropsOr(_, _) |
954            swagger20::Type::Patch |
955            swagger20::Type::WatchEvent(_)
956        ) => Ok(false),
957
958        _ => Ok(true),
959    })
960}
961
962fn evaluate_trait_bound(
963    kind: &swagger20::SchemaKind,
964    array_follows_elements: bool,
965    definitions: &std::collections::BTreeMap<swagger20::DefinitionPath, swagger20::Schema>,
966    map_namespace: &impl MapNamespace,
967    mut f: impl FnMut(&swagger20::SchemaKind, bool) -> Result<bool, Error>,
968) -> Result<bool, Error> {
969    fn evaluate_trait_bound_inner<'a>(
970        #[allow(clippy::ptr_arg)] // False positive. Clippy wants this to be `&SchemaKind` but we use Cow-specific operations (`.clone()`).
971        kind: &std::borrow::Cow<'a, swagger20::SchemaKind>,
972        required: bool,
973        array_follows_elements: bool,
974        definitions: &std::collections::BTreeMap<swagger20::DefinitionPath, swagger20::Schema>,
975        map_namespace: &impl MapNamespace,
976        visited: &mut std::collections::BTreeSet<std::borrow::Cow<'a, swagger20::SchemaKind>>,
977        f: &mut impl FnMut(&swagger20::SchemaKind, bool) -> Result<bool, Error>,
978    ) -> Result<bool, Error> {
979        if !visited.insert(kind.clone()) {
980            // In case of recursive types, assume the bound holds.
981            return Ok(true);
982        }
983
984        match &**kind {
985            swagger20::SchemaKind::Properties(properties) => {
986                for (property_schema, property_required) in properties.values() {
987                    let mut visited = visited.clone();
988                    let field_bound =
989                        evaluate_trait_bound_inner(
990                            &std::borrow::Cow::Borrowed(&property_schema.kind),
991                            required && *property_required,
992                            array_follows_elements,
993                            definitions,
994                            map_namespace,
995                            &mut visited,
996                            f,
997                        )?;
998                    if !field_bound {
999                        return Ok(false);
1000                    }
1001                }
1002
1003                Ok(true)
1004            },
1005
1006            swagger20::SchemaKind::Ref(ref_path @ swagger20::RefPath { .. }) if !ref_path.references_scope(map_namespace) => {
1007                let trait_bound =
1008                    if let Some(target) = definitions.get(&*ref_path.path) {
1009                        let mut visited = visited.clone();
1010                        evaluate_trait_bound_inner(
1011                            &std::borrow::Cow::Borrowed(&target.kind),
1012                            required,
1013                            array_follows_elements,
1014                            definitions,
1015                            map_namespace,
1016                            &mut visited,
1017                            f,
1018                        )
1019                    }
1020                    else {
1021                        f(kind, required)
1022                    };
1023                trait_bound
1024            },
1025
1026            swagger20::SchemaKind::Ty(swagger20::Type::Array { items }) if array_follows_elements =>
1027                evaluate_trait_bound_inner(
1028                    &std::borrow::Cow::Owned(items.kind.clone()),
1029                    required,
1030                    array_follows_elements,
1031                    definitions,
1032                    map_namespace,
1033                    visited,
1034                    f,
1035                ),
1036
1037            swagger20::SchemaKind::Ty(swagger20::Type::JsonSchemaPropsOr(namespace, _)) => {
1038                let json_schema_props_ref_path = swagger20::RefPath {
1039                    path: format!("io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.{namespace}.JSONSchemaProps"),
1040                    can_be_default: None,
1041                };
1042                let json_schema_props_bound =
1043                    evaluate_trait_bound_inner(
1044                        &std::borrow::Cow::Owned(swagger20::SchemaKind::Ref(json_schema_props_ref_path)),
1045                        required,
1046                        array_follows_elements,
1047                        definitions,
1048                        map_namespace,
1049                        visited,
1050                        f,
1051                    )?;
1052                if !json_schema_props_bound {
1053                    return Ok(false);
1054                }
1055
1056                f(kind, required)
1057            },
1058
1059            swagger20::SchemaKind::Ty(swagger20::Type::WatchEvent(raw_extension_ref_path)) => {
1060                let raw_extension_bound =
1061                    evaluate_trait_bound_inner(
1062                        &std::borrow::Cow::Owned(swagger20::SchemaKind::Ref(raw_extension_ref_path.clone())),
1063                        required,
1064                        array_follows_elements,
1065                        definitions,
1066                        map_namespace,
1067                        visited,
1068                        f,
1069                    )?;
1070                if !raw_extension_bound {
1071                    return Ok(false);
1072                }
1073
1074                f(kind, required)
1075            },
1076
1077            kind => f(kind, required),
1078        }
1079    }
1080
1081    let mut visited = Default::default();
1082    evaluate_trait_bound_inner(
1083        &std::borrow::Cow::Borrowed(kind),
1084        true,
1085        array_follows_elements,
1086        definitions,
1087        map_namespace,
1088        &mut visited,
1089        &mut f,
1090    )
1091}
1092
1093fn get_comment_text<'a>(s: &'a str, indent: &'a str) -> impl Iterator<Item = std::borrow::Cow<'static, str>> + 'a {
1094    s.lines().scan(true, move |previous_line_was_empty, line|
1095        if line.is_empty() {
1096            *previous_line_was_empty = true;
1097            Some("".into())
1098        }
1099        else {
1100            let line =
1101                line
1102                .replace('\\', r"\\")
1103                .replace('[', r"\[")
1104                .replace(']', r"\]")
1105                .replace('<', r"\<")
1106                .replace('>', r"\>")
1107                .replace('\t', "    ")
1108                .replace("```", "");
1109
1110            let line =
1111                if *previous_line_was_empty && line.starts_with("    ") {
1112                    // Collapse this line's spaces into two. Otherwise rustdoc will think this is the start of a code block containing a Rust test.
1113                    format!("  {}", line.trim_start())
1114                }
1115                else {
1116                    line
1117                };
1118            let line = line.trim_end();
1119
1120            *previous_line_was_empty = false;
1121
1122            Some(format!("{indent} {line}").into())
1123        })
1124}
1125
1126fn get_fully_qualified_type_name(
1127    ref_path: &swagger20::RefPath,
1128    map_namespace: &impl MapNamespace,
1129) -> String {
1130    let path_parts: Vec<_> = ref_path.path.split('.').collect();
1131    let namespace_parts = map_namespace.map_namespace(&path_parts[..(path_parts.len() - 1)]);
1132    if let Some(namespace_parts) = namespace_parts {
1133        let mut result = String::new();
1134        for namespace_part in namespace_parts {
1135            result.push_str(&get_rust_ident(namespace_part));
1136            result.push_str("::");
1137        }
1138        result.push_str(path_parts[path_parts.len() - 1]);
1139        result
1140    }
1141    else {
1142        let last_part = *path_parts.last().expect("str::split yields at least one item");
1143        last_part.to_owned()
1144    }
1145}
1146
1147/// Converts the given string into a string that can be used as a Rust ident.
1148pub fn get_rust_ident(name: &str) -> std::borrow::Cow<'static, str> {
1149    // Fix cases of invalid rust idents
1150    match name {
1151        "$ref" => return "ref_path".into(),
1152        "$schema" => return "schema".into(),
1153        "as" => return "as_".into(),
1154        "continue" => return "continue_".into(),
1155        "enum" => return "enum_".into(),
1156        "ref" => return "ref_".into(),
1157        "type" => return "type_".into(),
1158        _ => (),
1159    }
1160
1161    // Some cases of "ABc" should be converted to "abc" instead of "a_bc".
1162    // Eg "JSONSchemas" => "json_schemas", but "externalIPs" => "external_ips" instead of "external_i_ps".
1163    // Mostly happens with plurals of abbreviations.
1164    match name {
1165        "clusterIPs" => return "cluster_ips".into(),
1166        "externalIPs" => return "external_ips".into(),
1167        "hostIPs" => return "host_ips".into(),
1168        "nonResourceURLs" => return "non_resource_urls".into(),
1169        "podCIDRs" => return "pod_cidrs".into(),
1170        "podIPs" => return "pod_ips".into(),
1171        "serverAddressByClientCIDRs" => return "server_address_by_client_cidrs".into(),
1172        "targetWWNs" => return "target_wwns".into(),
1173        _ => (),
1174    }
1175
1176    let mut result = String::new();
1177
1178    let chars =
1179        name.chars()
1180        .zip(std::iter::once(None).chain(name.chars().map(|c| Some(c.is_uppercase()))))
1181        .zip(name.chars().skip(1).map(|c| Some(c.is_uppercase())).chain(std::iter::once(None)));
1182
1183    for ((c, previous), next) in chars {
1184        if c.is_uppercase() {
1185            match (previous, next) {
1186                (Some(false), _) |
1187                (Some(true), Some(false)) => result.push('_'),
1188                _ => (),
1189            }
1190
1191            result.extend(c.to_lowercase());
1192        }
1193        else {
1194            result.push(match c {
1195                '-' => '_',
1196                c => c,
1197            });
1198        }
1199    }
1200
1201    result.into()
1202}
1203
1204fn get_rust_type(
1205    schema_kind: &swagger20::SchemaKind,
1206    map_namespace: &impl MapNamespace,
1207) -> Result<std::borrow::Cow<'static, str>, Error> {
1208    let local = map_namespace_local_to_string(map_namespace)?;
1209
1210    match schema_kind {
1211        swagger20::SchemaKind::Properties(_) => Err("Nested anonymous types not supported".into()),
1212
1213        swagger20::SchemaKind::Ref(ref_path) =>
1214            Ok(get_fully_qualified_type_name(ref_path, map_namespace).into()),
1215
1216        swagger20::SchemaKind::Ty(swagger20::Type::Any) => Ok(format!("{local}serde_json::Value").into()),
1217
1218        swagger20::SchemaKind::Ty(swagger20::Type::Array { items }) =>
1219            Ok(format!("std::vec::Vec<{}>", get_rust_type(&items.kind, map_namespace)?).into()),
1220
1221        swagger20::SchemaKind::Ty(swagger20::Type::Boolean) => Ok("bool".into()),
1222
1223        swagger20::SchemaKind::Ty(swagger20::Type::Integer { format: swagger20::IntegerFormat::Int32 }) => Ok("i32".into()),
1224        swagger20::SchemaKind::Ty(swagger20::Type::Integer { format: swagger20::IntegerFormat::Int64 }) => Ok("i64".into()),
1225
1226        swagger20::SchemaKind::Ty(swagger20::Type::Number { format: swagger20::NumberFormat::Double }) => Ok("f64".into()),
1227
1228        swagger20::SchemaKind::Ty(swagger20::Type::Object { additional_properties }) =>
1229            Ok(format!("std::collections::BTreeMap<std::string::String, {}>", get_rust_type(&additional_properties.kind, map_namespace)?).into()),
1230
1231        swagger20::SchemaKind::Ty(swagger20::Type::String { format: Some(swagger20::StringFormat::Byte) }) =>
1232            Ok(format!("{local}ByteString").into()),
1233        swagger20::SchemaKind::Ty(swagger20::Type::String { format: Some(swagger20::StringFormat::DateTime) }) =>
1234            Ok(format!("{local}chrono::DateTime<{local}chrono::Utc>").into()),
1235        swagger20::SchemaKind::Ty(swagger20::Type::String { format: None }) => Ok("std::string::String".into()),
1236
1237        swagger20::SchemaKind::Ty(swagger20::Type::CustomResourceSubresources(namespace)) => {
1238            let namespace_parts =
1239                &["io", "k8s", "apiextensions_apiserver", "pkg", "apis", "apiextensions", namespace];
1240            let namespace_parts =
1241                map_namespace.map_namespace(namespace_parts)
1242                .ok_or_else(|| format!("unexpected path {:?}", namespace_parts.join(".")))?;
1243
1244            let mut result = String::new();
1245            for namespace_part in namespace_parts {
1246                result.push_str(&get_rust_ident(namespace_part));
1247                result.push_str("::");
1248            }
1249            result.push_str("CustomResourceSubresources");
1250            Ok(result.into())
1251        },
1252
1253        swagger20::SchemaKind::Ty(swagger20::Type::IntOrString) => Err("nothing should be trying to refer to IntOrString".into()),
1254        swagger20::SchemaKind::Ty(swagger20::Type::JsonSchemaPropsOr(_, _)) => Err("JSON schema types not supported".into()),
1255        swagger20::SchemaKind::Ty(swagger20::Type::Quantity) => Err("nothing should be trying to refer to Quantity".into()),
1256
1257        swagger20::SchemaKind::Ty(swagger20::Type::Patch) => Err("Patch type not supported".into()),
1258        swagger20::SchemaKind::Ty(swagger20::Type::WatchEvent(_)) => Err("WatchEvent type not supported".into()),
1259
1260        swagger20::SchemaKind::Ty(swagger20::Type::ListDef { .. }) => Err("ListDef type not supported".into()),
1261        swagger20::SchemaKind::Ty(swagger20::Type::ListRef { items }) =>
1262            Ok(format!("{local}List<{}>", get_rust_type(items, map_namespace)?).into()),
1263    }
1264}