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::Patch) => {
548            templates::patch::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::WatchEvent(raw_extension_ref_path)) => {
558            let error_status_rust_type = get_rust_type(
559                &swagger20::SchemaKind::Ref(swagger20::RefPath {
560                    path: "io.k8s.apimachinery.pkg.apis.meta.v1.Status".to_owned(),
561                    can_be_default: None,
562                }),
563                map_namespace,
564            )?;
565
566            let error_other_rust_type = get_rust_type(
567                &swagger20::SchemaKind::Ref(raw_extension_ref_path.clone()),
568                map_namespace,
569            )?;
570
571            templates::watch_event::generate(
572                &mut out,
573                type_name,
574                &error_status_rust_type,
575                &error_other_rust_type,
576                map_namespace,
577            )?;
578
579            run_result.num_generated_structs += 1;
580        },
581
582        swagger20::SchemaKind::Ty(swagger20::Type::ListDef { metadata }) => {
583            let metadata_rust_type = get_rust_type(metadata, map_namespace)?;
584
585            let template_generics_where_part = format!("T: {local}ListableResource");
586            let template_generics = templates::Generics {
587                type_part: Some("T"),
588                where_part: Some(&template_generics_where_part),
589            };
590
591            let items_merge_type = swagger20::MergeType::List {
592                strategy: swagger20::KubernetesListType::Map,
593                keys: vec!["metadata().namespace".to_string(), "metadata().name".to_string()],
594                item_merge_type: Box::new(swagger20::MergeType::Default),
595            };
596
597            let template_properties = vec![
598                templates::Property {
599                    name: "items",
600                    comment: Some("List of objects."),
601                    field_name: "items".into(),
602                    field_type_name: "std::vec::Vec<T>".to_owned(),
603                    required: templates::PropertyRequired::Required { is_default: true },
604                    is_flattened: false,
605                    merge_type: &items_merge_type,
606                },
607
608                templates::Property {
609                    name: "metadata",
610                    comment: Some("Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds"),
611                    field_name: "metadata".into(),
612                    field_type_name: (*metadata_rust_type).to_owned(),
613                    required: templates::PropertyRequired::Required { is_default: true },
614                    is_flattened: false,
615                    merge_type: &swagger20::MergeType::Default,
616                },
617            ];
618
619            let template_resource_metadata = templates::ResourceMetadata {
620                api_version: "<T as crate::Resource>::API_VERSION",
621                group: "<T as crate::Resource>::GROUP",
622                kind: "<T as crate::ListableResource>::LIST_KIND",
623                version: "<T as crate::Resource>::VERSION",
624                list_kind: None,
625                metadata_ty: Some(&metadata_rust_type),
626                url_path_segment_and_scope: (r#""""#, "<T as crate::Resource>::Scope"),
627            };
628
629            templates::r#struct::generate(
630                &mut out,
631                vis,
632                type_name,
633                template_generics,
634                &template_properties,
635            )?;
636
637            templates::impl_resource::generate(
638                &mut out,
639                type_name,
640                template_generics,
641                map_namespace,
642                &template_resource_metadata,
643            )?;
644
645            templates::impl_listable_resource::generate(
646                &mut out,
647                type_name,
648                template_generics,
649                map_namespace,
650                &template_resource_metadata,
651            )?;
652
653            templates::impl_metadata::generate(
654                &mut out,
655                type_name,
656                template_generics,
657                map_namespace,
658                &template_resource_metadata,
659            )?;
660
661            if definition.impl_deep_merge {
662                let template_generics_where_part = format!("T: {local}DeepMerge + {local}Metadata<Ty = {local}apimachinery::pkg::apis::meta::v1::ObjectMeta> + {local}ListableResource");
663                let template_generics = templates::Generics {
664                    where_part: Some(&template_generics_where_part),
665                    ..template_generics
666                };
667
668                templates::struct_deep_merge::generate(
669                    &mut out,
670                    type_name,
671                    template_generics,
672                    &template_properties,
673                    map_namespace,
674                )?;
675            }
676
677            {
678                let template_generics_where_part = format!("T: {local}serde::Deserialize<'de> + {local}ListableResource");
679                let template_generics = templates::Generics {
680                    where_part: Some(&template_generics_where_part),
681                    ..template_generics
682                };
683
684                templates::impl_deserialize::generate(
685                    &mut out,
686                    type_name,
687                    template_generics,
688                    &template_properties,
689                    map_namespace,
690                    Some(&template_resource_metadata),
691                )?;
692            }
693
694            {
695                let template_generics_where_part = format!("T: {local}serde::Serialize + {local}ListableResource");
696                let template_generics = templates::Generics {
697                    where_part: Some(&template_generics_where_part),
698                    ..template_generics
699                };
700
701                templates::impl_serialize::generate(
702                    &mut out,
703                    type_name,
704                    template_generics,
705                    &template_properties,
706                    map_namespace,
707                    Some(&template_resource_metadata),
708                )?;
709            }
710
711            run_result.num_generated_structs += 1;
712        },
713
714        swagger20::SchemaKind::Ty(swagger20::Type::ListRef { .. }) => return Err(format!("definition {definition_path} is a ListRef").into()),
715
716        swagger20::SchemaKind::Ty(_) => {
717            let inner_type_name = get_rust_type(&definition.kind, map_namespace)?;
718
719            // Kubernetes requires MicroTime to be serialized with exactly six decimal digits, instead of the default serde serialization of `chrono::DateTime`
720            // that uses a variable number up to nine.
721            //
722            // Furthermore, while Kubernetes does deserialize a Time from a string with one or more decimal digits,
723            // 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
724            // the same behavior as the golang client.
725            //
726            // Refs:
727            // - https://github.com/Arnavion/k8s-openapi/issues/63
728            // - https://github.com/deislabs/krustlet/issues/5
729            // - https://github.com/kubernetes/apimachinery/issues/88
730            let datetime_serialization_format = match (&**definition_path, &definition.kind) {
731                (
732                    "io.k8s.apimachinery.pkg.apis.meta.v1.MicroTime",
733                    swagger20::SchemaKind::Ty(swagger20::Type::String { format: Some(swagger20::StringFormat::DateTime) }),
734                ) => templates::DateTimeSerializationFormat::SixDecimalDigits,
735
736                (
737                    "io.k8s.apimachinery.pkg.apis.meta.v1.Time",
738                    swagger20::SchemaKind::Ty(swagger20::Type::String { format: Some(swagger20::StringFormat::DateTime) }),
739                ) => templates::DateTimeSerializationFormat::ZeroDecimalDigits,
740
741                _ => templates::DateTimeSerializationFormat::Default,
742            };
743
744            templates::newtype::generate(
745                &mut out,
746                vis,
747                type_name,
748                &inner_type_name,
749                datetime_serialization_format,
750                map_namespace,
751            )?;
752
753            run_result.num_generated_type_aliases += 1;
754        },
755    }
756
757    if let GenerateSchema::Yes { feature: schema_feature } = generate_schema {
758        match &definition.kind {
759            swagger20::SchemaKind::Properties(_) |
760            swagger20::SchemaKind::Ty(
761                swagger20::Type::Any |
762                swagger20::Type::Array { .. } |
763                swagger20::Type::Boolean |
764                swagger20::Type::Integer { .. } |
765                swagger20::Type::IntOrString |
766                swagger20::Type::Number { .. } |
767                swagger20::Type::Object { .. } |
768                swagger20::Type::String { .. } |
769                swagger20::Type::JsonSchemaPropsOr(_, _) |
770                swagger20::Type::Patch
771            ) => {
772                templates::impl_schema::generate(
773                    &mut out,
774                    type_name,
775                    Default::default(),
776                    definition_path,
777                    definition,
778                    schema_feature,
779                    map_namespace,
780                )?;
781            }
782
783            swagger20::SchemaKind::Ty(swagger20::Type::WatchEvent(_)) => {
784                templates::impl_schema::generate(
785                    &mut out,
786                    type_name,
787                    templates::Generics {
788                        type_part: Some("T"),
789                        where_part: None,
790                    },
791                    definition_path,
792                    definition,
793                    schema_feature,
794                    map_namespace,
795                )?;
796            }
797
798            _ => (),
799        }
800    }
801
802    state.finish(out);
803
804    Ok(run_result)
805}
806
807fn map_namespace_local_to_string(map_namespace: &impl MapNamespace) -> Result<String, Error> {
808    let namespace_parts = map_namespace.map_namespace(&["io", "k8s"]).ok_or(r#"unexpected path "io.k8s""#)?;
809
810    let mut result = String::new();
811    for namespace_part in namespace_parts {
812        result.push_str(&get_rust_ident(namespace_part));
813        result.push_str("::");
814    }
815    Ok(result)
816}
817
818fn get_derives(
819    kind: &swagger20::SchemaKind,
820    definitions: &std::collections::BTreeMap<swagger20::DefinitionPath, swagger20::Schema>,
821    map_namespace: &impl MapNamespace,
822) -> Result<Option<templates::type_header::Derives>, Error> {
823    if matches!(kind, swagger20::SchemaKind::Ty(swagger20::Type::ListRef { .. })) {
824        // ListRef is emitted as a type alias.
825        return Ok(None);
826    }
827
828    let derive_clone =
829        evaluate_trait_bound(
830            kind,
831            true,
832            definitions,
833            map_namespace,
834            |_, _| Ok(true))?;
835
836    let derive_copy =
837        derive_clone &&
838        evaluate_trait_bound(
839            kind,
840            false,
841            definitions,
842            map_namespace,
843            |_, _| Ok(false))?;
844
845    #[allow(clippy::match_same_arms)]
846    let is_default = evaluate_trait_bound(kind, false, definitions, map_namespace, |kind, required| match kind {
847        // Option<T>::default is None regardless of T
848        _ if !required => Ok(true),
849
850        swagger20::SchemaKind::Ref(swagger20::RefPath { can_be_default: Some(can_be_default), .. }) => Ok(*can_be_default),
851
852        swagger20::SchemaKind::Ref(ref_path @ swagger20::RefPath { .. }) if ref_path.references_scope(map_namespace) => Ok(false),
853
854        // metadata field in resource type created by #[derive(CustomResourceDefinition)]
855        swagger20::SchemaKind::Ref(ref_path @ swagger20::RefPath { .. })
856            if !ref_path.references_scope(map_namespace) && ref_path.path == "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" => Ok(true),
857
858        // Handled by evaluate_trait_bound
859        swagger20::SchemaKind::Ref(ref_path @ swagger20::RefPath { .. }) if !ref_path.references_scope(map_namespace) => unreachable!(),
860
861        // chrono::DateTime<chrono::Utc> is not Default
862        swagger20::SchemaKind::Ty(swagger20::Type::String { format: Some(swagger20::StringFormat::DateTime) }) => Ok(false),
863
864        // Enums without a default value
865        swagger20::SchemaKind::Ty(
866            swagger20::Type::JsonSchemaPropsOr(_, _) |
867            swagger20::Type::Patch |
868            swagger20::Type::WatchEvent(_)
869        ) => Ok(false),
870
871        _ => Ok(true),
872    })?;
873    let derive_default =
874        is_default &&
875        // IntOrString has a manual Default impl, so don't #[derive] it.
876        !matches!(kind, swagger20::SchemaKind::Ty(swagger20::Type::IntOrString));
877
878    let derive_partial_eq =
879        evaluate_trait_bound(
880            kind,
881            true,
882            definitions,
883            map_namespace,
884            |_, _| Ok(true))?;
885
886    // The choice of deriving Eq, Ord and PartialOrd is deliberately more conservative than the choice of deriving PartialEq,
887    // so as to not change dramatically between Kubernetes versions. For example, ObjectMeta is Ord in v1.15 but not in v1.16 because
888    // it indirectly gained a serde_json::Value field (`managed_fields.fields_v1.0`).
889    //
890    // Also, being conservative means the types generated by #[derive(k8s_openapi_derive::CustomResource)] don't have to require them either.
891
892    let derive_eq =
893        derive_partial_eq &&
894        matches!(kind, swagger20::SchemaKind::Ty(
895            swagger20::Type::IntOrString |
896            swagger20::Type::String { format: Some(swagger20::StringFormat::DateTime) }
897        ));
898
899    let derive_partial_ord =
900        derive_partial_eq &&
901        matches!(kind, swagger20::SchemaKind::Ty(swagger20::Type::String { format: Some(swagger20::StringFormat::DateTime) }));
902
903    let derive_ord = derive_partial_ord && derive_eq;
904
905    Ok(Some(templates::type_header::Derives {
906        clone: derive_clone,
907        copy: derive_copy,
908        default: derive_default,
909        eq: derive_eq,
910        ord: derive_ord,
911        partial_eq: derive_partial_eq,
912        partial_ord: derive_partial_ord,
913    }))
914}
915
916fn is_default(
917    kind: &swagger20::SchemaKind,
918    definitions: &std::collections::BTreeMap<swagger20::DefinitionPath, swagger20::Schema>,
919    map_namespace: &impl MapNamespace,
920) -> Result<bool, Error> {
921    #[allow(clippy::match_same_arms)]
922    evaluate_trait_bound(kind, false, definitions, map_namespace, |kind, required| match kind {
923        // Option<T>::default is None regardless of T
924        _ if !required => Ok(true),
925
926        swagger20::SchemaKind::Ref(swagger20::RefPath { can_be_default: Some(can_be_default), .. }) => Ok(*can_be_default),
927
928        swagger20::SchemaKind::Ref(ref_path @ swagger20::RefPath { .. }) if ref_path.references_scope(map_namespace) => Ok(false),
929
930        // metadata field in resource type created by #[derive(CustomResourceDefinition)]
931        swagger20::SchemaKind::Ref(ref_path @ swagger20::RefPath { .. })
932            if !ref_path.references_scope(map_namespace) && ref_path.path == "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" => Ok(true),
933
934        // Handled by evaluate_trait_bound
935        swagger20::SchemaKind::Ref(ref_path @ swagger20::RefPath { .. }) if !ref_path.references_scope(map_namespace) => unreachable!(),
936
937        // chrono::DateTime<chrono::Utc> is not Default
938        swagger20::SchemaKind::Ty(swagger20::Type::String { format: Some(swagger20::StringFormat::DateTime) }) => Ok(false),
939
940        // Enums without a default value
941        swagger20::SchemaKind::Ty(
942            swagger20::Type::JsonSchemaPropsOr(_, _) |
943            swagger20::Type::Patch |
944            swagger20::Type::WatchEvent(_)
945        ) => Ok(false),
946
947        _ => Ok(true),
948    })
949}
950
951fn evaluate_trait_bound(
952    kind: &swagger20::SchemaKind,
953    array_follows_elements: bool,
954    definitions: &std::collections::BTreeMap<swagger20::DefinitionPath, swagger20::Schema>,
955    map_namespace: &impl MapNamespace,
956    mut f: impl FnMut(&swagger20::SchemaKind, bool) -> Result<bool, Error>,
957) -> Result<bool, Error> {
958    fn evaluate_trait_bound_inner<'a>(
959        #[allow(clippy::ptr_arg)] // False positive. Clippy wants this to be `&SchemaKind` but we use Cow-specific operations (`.clone()`).
960        kind: &std::borrow::Cow<'a, swagger20::SchemaKind>,
961        required: bool,
962        array_follows_elements: bool,
963        definitions: &std::collections::BTreeMap<swagger20::DefinitionPath, swagger20::Schema>,
964        map_namespace: &impl MapNamespace,
965        visited: &mut std::collections::BTreeSet<std::borrow::Cow<'a, swagger20::SchemaKind>>,
966        f: &mut impl FnMut(&swagger20::SchemaKind, bool) -> Result<bool, Error>,
967    ) -> Result<bool, Error> {
968        if !visited.insert(kind.clone()) {
969            // In case of recursive types, assume the bound holds.
970            return Ok(true);
971        }
972
973        match &**kind {
974            swagger20::SchemaKind::Properties(properties) => {
975                for (property_schema, property_required) in properties.values() {
976                    let mut visited = visited.clone();
977                    let field_bound =
978                        evaluate_trait_bound_inner(
979                            &std::borrow::Cow::Borrowed(&property_schema.kind),
980                            required && *property_required,
981                            array_follows_elements,
982                            definitions,
983                            map_namespace,
984                            &mut visited,
985                            f,
986                        )?;
987                    if !field_bound {
988                        return Ok(false);
989                    }
990                }
991
992                Ok(true)
993            },
994
995            swagger20::SchemaKind::Ref(ref_path @ swagger20::RefPath { .. }) if !ref_path.references_scope(map_namespace) => {
996                let trait_bound =
997                    if let Some(target) = definitions.get(&*ref_path.path) {
998                        let mut visited = visited.clone();
999                        evaluate_trait_bound_inner(
1000                            &std::borrow::Cow::Borrowed(&target.kind),
1001                            required,
1002                            array_follows_elements,
1003                            definitions,
1004                            map_namespace,
1005                            &mut visited,
1006                            f,
1007                        )
1008                    }
1009                    else {
1010                        f(kind, required)
1011                    };
1012                trait_bound
1013            },
1014
1015            swagger20::SchemaKind::Ty(swagger20::Type::Array { items }) if array_follows_elements =>
1016                evaluate_trait_bound_inner(
1017                    &std::borrow::Cow::Owned(items.kind.clone()),
1018                    required,
1019                    array_follows_elements,
1020                    definitions,
1021                    map_namespace,
1022                    visited,
1023                    f,
1024                ),
1025
1026            swagger20::SchemaKind::Ty(swagger20::Type::JsonSchemaPropsOr(namespace, _)) => {
1027                let json_schema_props_ref_path = swagger20::RefPath {
1028                    path: format!("io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.{namespace}.JSONSchemaProps"),
1029                    can_be_default: None,
1030                };
1031                let json_schema_props_bound =
1032                    evaluate_trait_bound_inner(
1033                        &std::borrow::Cow::Owned(swagger20::SchemaKind::Ref(json_schema_props_ref_path)),
1034                        required,
1035                        array_follows_elements,
1036                        definitions,
1037                        map_namespace,
1038                        visited,
1039                        f,
1040                    )?;
1041                if !json_schema_props_bound {
1042                    return Ok(false);
1043                }
1044
1045                f(kind, required)
1046            },
1047
1048            swagger20::SchemaKind::Ty(swagger20::Type::WatchEvent(raw_extension_ref_path)) => {
1049                let raw_extension_bound =
1050                    evaluate_trait_bound_inner(
1051                        &std::borrow::Cow::Owned(swagger20::SchemaKind::Ref(raw_extension_ref_path.clone())),
1052                        required,
1053                        array_follows_elements,
1054                        definitions,
1055                        map_namespace,
1056                        visited,
1057                        f,
1058                    )?;
1059                if !raw_extension_bound {
1060                    return Ok(false);
1061                }
1062
1063                f(kind, required)
1064            },
1065
1066            kind => f(kind, required),
1067        }
1068    }
1069
1070    let mut visited = Default::default();
1071    evaluate_trait_bound_inner(
1072        &std::borrow::Cow::Borrowed(kind),
1073        true,
1074        array_follows_elements,
1075        definitions,
1076        map_namespace,
1077        &mut visited,
1078        &mut f,
1079    )
1080}
1081
1082fn get_comment_text<'a>(s: &'a str, indent: &'a str) -> impl Iterator<Item = std::borrow::Cow<'static, str>> + 'a {
1083    s.lines().scan(true, move |previous_line_was_empty, line|
1084        if line.is_empty() {
1085            *previous_line_was_empty = true;
1086            Some("".into())
1087        }
1088        else {
1089            let line =
1090                line
1091                .replace('\\', r"\\")
1092                .replace('[', r"\[")
1093                .replace(']', r"\]")
1094                .replace('<', r"\<")
1095                .replace('>', r"\>")
1096                .replace('\t', "    ")
1097                .replace("```", "");
1098
1099            let line =
1100                if *previous_line_was_empty && line.starts_with("    ") {
1101                    // Collapse this line's spaces into two. Otherwise rustdoc will think this is the start of a code block containing a Rust test.
1102                    format!("  {}", line.trim_start())
1103                }
1104                else {
1105                    line
1106                };
1107            let line = line.trim_end();
1108
1109            *previous_line_was_empty = false;
1110
1111            Some(format!("{indent} {line}").into())
1112        })
1113}
1114
1115fn get_fully_qualified_type_name(
1116    ref_path: &swagger20::RefPath,
1117    map_namespace: &impl MapNamespace,
1118) -> String {
1119    let path_parts: Vec<_> = ref_path.path.split('.').collect();
1120    let namespace_parts = map_namespace.map_namespace(&path_parts[..(path_parts.len() - 1)]);
1121    if let Some(namespace_parts) = namespace_parts {
1122        let mut result = String::new();
1123        for namespace_part in namespace_parts {
1124            result.push_str(&get_rust_ident(namespace_part));
1125            result.push_str("::");
1126        }
1127        result.push_str(path_parts[path_parts.len() - 1]);
1128        result
1129    }
1130    else {
1131        let last_part = *path_parts.last().expect("str::split yields at least one item");
1132        last_part.to_owned()
1133    }
1134}
1135
1136/// Converts the given string into a string that can be used as a Rust ident.
1137pub fn get_rust_ident(name: &str) -> std::borrow::Cow<'static, str> {
1138    // Fix cases of invalid rust idents
1139    match name {
1140        "$ref" => return "ref_path".into(),
1141        "$schema" => return "schema".into(),
1142        "as" => return "as_".into(),
1143        "continue" => return "continue_".into(),
1144        "enum" => return "enum_".into(),
1145        "ref" => return "ref_".into(),
1146        "type" => return "type_".into(),
1147        _ => (),
1148    }
1149
1150    // Some cases of "ABc" should be converted to "abc" instead of "a_bc".
1151    // Eg "JSONSchemas" => "json_schemas", but "externalIPs" => "external_ips" instead of "external_i_ps".
1152    // Mostly happens with plurals of abbreviations.
1153    match name {
1154        "clusterIPs" => return "cluster_ips".into(),
1155        "externalIPs" => return "external_ips".into(),
1156        "hostIPs" => return "host_ips".into(),
1157        "nonResourceURLs" => return "non_resource_urls".into(),
1158        "podCIDRs" => return "pod_cidrs".into(),
1159        "podIPs" => return "pod_ips".into(),
1160        "serverAddressByClientCIDRs" => return "server_address_by_client_cidrs".into(),
1161        "targetWWNs" => return "target_wwns".into(),
1162        _ => (),
1163    }
1164
1165    let mut result = String::new();
1166
1167    let chars =
1168        name.chars()
1169        .zip(std::iter::once(None).chain(name.chars().map(|c| Some(c.is_uppercase()))))
1170        .zip(name.chars().skip(1).map(|c| Some(c.is_uppercase())).chain(std::iter::once(None)));
1171
1172    for ((c, previous), next) in chars {
1173        if c.is_uppercase() {
1174            match (previous, next) {
1175                (Some(false), _) |
1176                (Some(true), Some(false)) => result.push('_'),
1177                _ => (),
1178            }
1179
1180            result.extend(c.to_lowercase());
1181        }
1182        else {
1183            result.push(match c {
1184                '-' => '_',
1185                c => c,
1186            });
1187        }
1188    }
1189
1190    result.into()
1191}
1192
1193fn get_rust_type(
1194    schema_kind: &swagger20::SchemaKind,
1195    map_namespace: &impl MapNamespace,
1196) -> Result<std::borrow::Cow<'static, str>, Error> {
1197    let local = map_namespace_local_to_string(map_namespace)?;
1198
1199    match schema_kind {
1200        swagger20::SchemaKind::Properties(_) => Err("Nested anonymous types not supported".into()),
1201
1202        swagger20::SchemaKind::Ref(ref_path) =>
1203            Ok(get_fully_qualified_type_name(ref_path, map_namespace).into()),
1204
1205        swagger20::SchemaKind::Ty(swagger20::Type::Any) => Ok(format!("{local}serde_json::Value").into()),
1206
1207        swagger20::SchemaKind::Ty(swagger20::Type::Array { items }) =>
1208            Ok(format!("std::vec::Vec<{}>", get_rust_type(&items.kind, map_namespace)?).into()),
1209
1210        swagger20::SchemaKind::Ty(swagger20::Type::Boolean) => Ok("bool".into()),
1211
1212        swagger20::SchemaKind::Ty(swagger20::Type::Integer { format: swagger20::IntegerFormat::Int32 }) => Ok("i32".into()),
1213        swagger20::SchemaKind::Ty(swagger20::Type::Integer { format: swagger20::IntegerFormat::Int64 }) => Ok("i64".into()),
1214
1215        swagger20::SchemaKind::Ty(swagger20::Type::Number { format: swagger20::NumberFormat::Double }) => Ok("f64".into()),
1216
1217        swagger20::SchemaKind::Ty(swagger20::Type::Object { additional_properties }) =>
1218            Ok(format!("std::collections::BTreeMap<std::string::String, {}>", get_rust_type(&additional_properties.kind, map_namespace)?).into()),
1219
1220        swagger20::SchemaKind::Ty(swagger20::Type::String { format: Some(swagger20::StringFormat::Byte) }) =>
1221            Ok(format!("{local}ByteString").into()),
1222        swagger20::SchemaKind::Ty(swagger20::Type::String { format: Some(swagger20::StringFormat::DateTime) }) =>
1223            Ok(format!("{local}chrono::DateTime<{local}chrono::Utc>").into()),
1224        swagger20::SchemaKind::Ty(swagger20::Type::String { format: None }) => Ok("std::string::String".into()),
1225
1226        swagger20::SchemaKind::Ty(swagger20::Type::CustomResourceSubresources(namespace)) => {
1227            let namespace_parts =
1228                &["io", "k8s", "apiextensions_apiserver", "pkg", "apis", "apiextensions", namespace];
1229            let namespace_parts =
1230                map_namespace.map_namespace(namespace_parts)
1231                .ok_or_else(|| format!("unexpected path {:?}", namespace_parts.join(".")))?;
1232
1233            let mut result = String::new();
1234            for namespace_part in namespace_parts {
1235                result.push_str(&get_rust_ident(namespace_part));
1236                result.push_str("::");
1237            }
1238            result.push_str("CustomResourceSubresources");
1239            Ok(result.into())
1240        },
1241
1242        swagger20::SchemaKind::Ty(swagger20::Type::IntOrString) => Err("nothing should be trying to refer to IntOrString".into()),
1243
1244        swagger20::SchemaKind::Ty(swagger20::Type::JsonSchemaPropsOr(_, _)) => Err("JSON schema types not supported".into()),
1245        swagger20::SchemaKind::Ty(swagger20::Type::Patch) => Err("Patch type not supported".into()),
1246        swagger20::SchemaKind::Ty(swagger20::Type::WatchEvent(_)) => Err("WatchEvent type not supported".into()),
1247
1248        swagger20::SchemaKind::Ty(swagger20::Type::ListDef { .. }) => Err("ListDef type not supported".into()),
1249        swagger20::SchemaKind::Ty(swagger20::Type::ListRef { items }) =>
1250            Ok(format!("{local}List<{}>", get_rust_type(items, map_namespace)?).into()),
1251    }
1252}