Skip to main content

ploidy_codegen_rust/
schema.rs

1use ploidy_core::{
2    codegen::IntoCode,
3    ir::{ContainerView, HasTypeId, SchemaTypeView, View},
4};
5use proc_macro2::TokenStream;
6use quote::{ToTokens, TokenStreamExt, quote};
7
8use super::{
9    doc_attrs, enum_::CodegenEnum, graph::CodegenGraph, inlines::CodegenInlines,
10    naming::CodegenIdentUsage, primitive::CodegenPrimitive, ref_::CodegenRef,
11    struct_::CodegenStruct, tagged::CodegenTagged, untagged::CodegenUntagged,
12};
13
14/// Generates a module for a named schema type.
15#[derive(Debug)]
16pub struct CodegenSchemaType<'a> {
17    graph: &'a CodegenGraph<'a>,
18    ty: &'a SchemaTypeView<'a, 'a>,
19}
20
21impl<'a> CodegenSchemaType<'a> {
22    pub fn new(graph: &'a CodegenGraph<'a>, ty: &'a SchemaTypeView<'a, 'a>) -> Self {
23        Self { graph, ty }
24    }
25}
26
27impl ToTokens for CodegenSchemaType<'_> {
28    fn to_tokens(&self, tokens: &mut TokenStream) {
29        let ty = match self.ty {
30            SchemaTypeView::Struct(_, view) => {
31                CodegenStruct::new(self.graph, view).into_token_stream()
32            }
33            SchemaTypeView::Enum(_, view) => CodegenEnum::new(self.graph, view).into_token_stream(),
34            SchemaTypeView::Tagged(_, view) => {
35                CodegenTagged::new(self.graph, view).into_token_stream()
36            }
37            SchemaTypeView::Untagged(_, view) => {
38                CodegenUntagged::new(self.graph, view).into_token_stream()
39            }
40            SchemaTypeView::Container(_, ContainerView::Array(inner)) => {
41                let doc_attrs = inner.description().map(doc_attrs);
42                let type_name = CodegenIdentUsage::Type(self.graph.ident(self.ty.id()));
43                let inner_ty = inner.ty();
44                let inner_ref = CodegenRef::new(self.graph, &inner_ty);
45                quote! {
46                    #doc_attrs
47                    pub type #type_name = ::std::vec::Vec<#inner_ref>;
48                }
49            }
50            SchemaTypeView::Container(_, ContainerView::Map(inner)) => {
51                let doc_attrs = inner.description().map(doc_attrs);
52                let type_name = CodegenIdentUsage::Type(self.graph.ident(self.ty.id()));
53                let inner_ty = inner.ty();
54                let inner_ref = CodegenRef::new(self.graph, &inner_ty);
55                quote! {
56                    #doc_attrs
57                    pub type #type_name = ::std::collections::BTreeMap<::std::string::String, #inner_ref>;
58                }
59            }
60            SchemaTypeView::Container(_, ContainerView::Optional(inner)) => {
61                let doc_attrs = inner.description().map(doc_attrs);
62                let type_name = CodegenIdentUsage::Type(self.graph.ident(self.ty.id()));
63                let inner_ty = inner.ty();
64                let inner_ref = CodegenRef::new(self.graph, &inner_ty);
65                quote! {
66                    #doc_attrs
67                    pub type #type_name = ::std::option::Option<#inner_ref>;
68                }
69            }
70            SchemaTypeView::Primitive(_, view) => {
71                let type_name = CodegenIdentUsage::Type(self.graph.ident(self.ty.id()));
72                let primitive = CodegenPrimitive::new(self.graph, view);
73                quote! {
74                    pub type #type_name = #primitive;
75                }
76            }
77            SchemaTypeView::Any(_, _) => {
78                let type_name = CodegenIdentUsage::Type(self.graph.ident(self.ty.id()));
79                quote! {
80                    pub type #type_name = ::ploidy_util::serde_json::Value;
81                }
82            }
83        };
84        let inlines = CodegenInlines::for_schema_inlines(self.graph, self.ty.inlines().collect());
85        tokens.append_all(quote! {
86            #ty
87            #inlines
88        });
89    }
90}
91
92impl IntoCode for CodegenSchemaType<'_> {
93    type Code = (String, TokenStream);
94
95    fn into_code(self) -> Self::Code {
96        let mod_name = CodegenIdentUsage::Module(self.graph.ident(self.ty.id()));
97        (
98            format!("src/types/{}.rs", mod_name.display()),
99            self.into_token_stream(),
100        )
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    use ploidy_core::{
109        arena::Arena,
110        ir::{RawGraph, SchemaTypeView, Spec},
111        parse::Document,
112    };
113    use pretty_assertions::assert_eq;
114    use syn::parse_quote;
115
116    use crate::CodegenGraph;
117
118    #[test]
119    fn test_schema_inline_types_order() {
120        // Inline types are defined in reverse alphabetical order (Zebra, Mango, Apple),
121        // to verify that they're sorted in the output.
122        let doc = Document::from_yaml(indoc::indoc! {"
123            openapi: 3.0.0
124            info:
125              title: Test API
126              version: 1.0.0
127            paths: {}
128            components:
129              schemas:
130                Container:
131                  type: object
132                  properties:
133                    zebra:
134                      type: object
135                      properties:
136                        name:
137                          type: string
138                    mango:
139                      type: object
140                      properties:
141                        name:
142                          type: string
143                    apple:
144                      type: object
145                      properties:
146                        name:
147                          type: string
148        "})
149        .unwrap();
150
151        let arena = Arena::new();
152        let spec = Spec::from_doc(&arena, &doc).unwrap();
153        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
154
155        let schema = graph.schema("Container").unwrap();
156        let SchemaTypeView::Struct(_, _) = &schema else {
157            panic!("expected struct `Container`; got `{schema:?}`");
158        };
159
160        let codegen = CodegenSchemaType::new(&graph, &schema);
161
162        let actual: syn::File = parse_quote!(#codegen);
163        // The struct fields remain in their original order (`zebra`, `mango`, `apple`),
164        // but the inline types in `mod types` should be sorted alphabetically
165        // (`Apple`, `Mango`, `Zebra`).
166        let expected: syn::File = parse_quote! {
167            #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
168            #[serde(crate = "::ploidy_util::serde")]
169            #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
170            pub struct Container {
171                #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
172                pub zebra: ::ploidy_util::absent::AbsentOr<crate::types::container::types::Zebra>,
173                #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
174                pub mango: ::ploidy_util::absent::AbsentOr<crate::types::container::types::Mango>,
175                #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
176                pub apple: ::ploidy_util::absent::AbsentOr<crate::types::container::types::Apple>,
177            }
178            pub mod types {
179                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
180                #[serde(crate = "::ploidy_util::serde")]
181                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
182                pub struct Apple {
183                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
184                    pub name: ::ploidy_util::absent::AbsentOr<::std::string::String>,
185                }
186                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
187                #[serde(crate = "::ploidy_util::serde")]
188                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
189                pub struct Mango {
190                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
191                    pub name: ::ploidy_util::absent::AbsentOr<::std::string::String>,
192                }
193                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
194                #[serde(crate = "::ploidy_util::serde")]
195                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
196                pub struct Zebra {
197                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
198                    pub name: ::ploidy_util::absent::AbsentOr<::std::string::String>,
199                }
200            }
201        };
202        assert_eq!(actual, expected);
203    }
204
205    #[test]
206    fn test_container_schema_emits_type_alias_with_inline_types() {
207        // A named array of inline structs should emit a type alias for the array,
208        // and a `mod types` with the inline type (linabutler/ploidy#30).
209        let doc = Document::from_yaml(indoc::indoc! {"
210            openapi: 3.0.0
211            info:
212              title: Test API
213              version: 1.0.0
214            paths: {}
215            components:
216              schemas:
217                InvalidParameters:
218                  type: array
219                  items:
220                    type: object
221                    required:
222                      - name
223                      - reason
224                    properties:
225                      name:
226                        type: string
227                      reason:
228                        type: string
229        "})
230        .unwrap();
231
232        let arena = Arena::new();
233        let spec = Spec::from_doc(&arena, &doc).unwrap();
234        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
235
236        let schema = graph.schema("InvalidParameters").unwrap();
237        let SchemaTypeView::Container(_, _) = &schema else {
238            panic!("expected container `InvalidParameters`; got `{schema:?}`");
239        };
240
241        let codegen = CodegenSchemaType::new(&graph, &schema);
242
243        let actual: syn::File = parse_quote!(#codegen);
244        let expected: syn::File = parse_quote! {
245            pub type InvalidParameters = ::std::vec::Vec<crate::types::invalid_parameters::types::Item>;
246            pub mod types {
247                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
248                #[serde(crate = "::ploidy_util::serde")]
249                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
250                pub struct Item {
251                    pub name: ::std::string::String,
252                    pub reason: ::std::string::String,
253                }
254            }
255        };
256        assert_eq!(actual, expected);
257    }
258
259    #[test]
260    fn test_container_schema_emits_type_alias_without_inline_types() {
261        // A named array of primitives should emit a type alias, and no `mod types`.
262        let doc = Document::from_yaml(indoc::indoc! {"
263            openapi: 3.0.0
264            info:
265              title: Test API
266              version: 1.0.0
267            paths: {}
268            components:
269              schemas:
270                Tags:
271                  type: array
272                  items:
273                    type: string
274        "})
275        .unwrap();
276
277        let arena = Arena::new();
278        let spec = Spec::from_doc(&arena, &doc).unwrap();
279        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
280
281        let schema = graph.schema("Tags").unwrap();
282        let SchemaTypeView::Container(_, _) = &schema else {
283            panic!("expected container `Tags`; got `{schema:?}`");
284        };
285
286        let codegen = CodegenSchemaType::new(&graph, &schema);
287
288        let actual: syn::File = parse_quote!(#codegen);
289        let expected: syn::File = parse_quote! {
290            pub type Tags = ::std::vec::Vec<::std::string::String>;
291        };
292        assert_eq!(actual, expected);
293    }
294
295    #[test]
296    fn test_container_schema_map_emits_type_alias() {
297        let doc = Document::from_yaml(indoc::indoc! {"
298            openapi: 3.0.0
299            info:
300              title: Test API
301              version: 1.0.0
302            paths: {}
303            components:
304              schemas:
305                Metadata:
306                  type: object
307                  additionalProperties:
308                    type: string
309        "})
310        .unwrap();
311
312        let arena = Arena::new();
313        let spec = Spec::from_doc(&arena, &doc).unwrap();
314        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
315
316        let schema = graph.schema("Metadata").unwrap();
317        let SchemaTypeView::Container(_, _) = &schema else {
318            panic!("expected container `Metadata`; got `{schema:?}`");
319        };
320
321        let codegen = CodegenSchemaType::new(&graph, &schema);
322
323        let actual: syn::File = parse_quote!(#codegen);
324        let expected: syn::File = parse_quote! {
325            pub type Metadata = ::std::collections::BTreeMap<::std::string::String, ::std::string::String>;
326        };
327        assert_eq!(actual, expected);
328    }
329
330    #[test]
331    fn test_container_nullable_schema() {
332        let doc = Document::from_yaml(indoc::indoc! {"
333            openapi: 3.1.0
334            info:
335              title: Test API
336              version: 1.0.0
337            paths: {}
338            components:
339              schemas:
340                NullableString:
341                  type: [string, 'null']
342                NullableArray:
343                  type: [array, 'null']
344                  items:
345                    type: string
346                NullableMap:
347                  type: [object, 'null']
348                  additionalProperties:
349                    type: string
350                NullableOneOf:
351                  oneOf:
352                    - type: object
353                      properties:
354                        value:
355                          type: string
356                    - type: 'null'
357        "})
358        .unwrap();
359
360        let arena = Arena::new();
361        let spec = Spec::from_doc(&arena, &doc).unwrap();
362        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
363
364        // `type: ["string", "null"]` becomes `Option<String>`.
365        let schema = graph.schema("NullableString").unwrap();
366        let SchemaTypeView::Container(_, _) = &schema else {
367            panic!("expected container `NullableString`; got `{schema:?}`");
368        };
369        let codegen = CodegenSchemaType::new(&graph, &schema);
370        let actual: syn::File = parse_quote!(#codegen);
371        let expected: syn::File = parse_quote! {
372            pub type NullableString = ::std::option::Option<::std::string::String>;
373        };
374        assert_eq!(actual, expected);
375
376        // `type: ["array", "null"]` becomes `Option<Vec<String>>`.
377        let schema = graph.schema("NullableArray").unwrap();
378        let SchemaTypeView::Container(_, _) = &schema else {
379            panic!("expected container `NullableArray`; got `{schema:?}`");
380        };
381        let codegen = CodegenSchemaType::new(&graph, &schema);
382        let actual: syn::File = parse_quote!(#codegen);
383        let expected: syn::File = parse_quote! {
384            pub type NullableArray = ::std::option::Option<::std::vec::Vec<::std::string::String>>;
385        };
386        assert_eq!(actual, expected);
387
388        // `type: ["object", "null"]` with `additionalProperties` becomes
389        // `Option<BTreeMap<String, String>>`.
390        let schema = graph.schema("NullableMap").unwrap();
391        let SchemaTypeView::Container(_, _) = &schema else {
392            panic!("expected container `NullableMap`; got `{schema:?}`");
393        };
394        let codegen = CodegenSchemaType::new(&graph, &schema);
395        let actual: syn::File = parse_quote!(#codegen);
396        let expected: syn::File = parse_quote! {
397            pub type NullableMap = ::std::option::Option<::std::collections::BTreeMap<::std::string::String, ::std::string::String>>;
398        };
399        assert_eq!(actual, expected);
400
401        // `oneOf` with an inline schema and `null` becomes an `Option<InlineStruct>`,
402        // with the inline struct definition emitted in `mod types`.
403        let schema = graph.schema("NullableOneOf").unwrap();
404        let SchemaTypeView::Container(_, _) = &schema else {
405            panic!("expected container `NullableOneOf`; got `{schema:?}`");
406        };
407        let codegen = CodegenSchemaType::new(&graph, &schema);
408        let actual: syn::File = parse_quote!(#codegen);
409        let expected: syn::File = parse_quote! {
410            pub type NullableOneOf = ::std::option::Option<crate::types::nullable_one_of::types::NullableOneOf>;
411            pub mod types {
412                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
413                #[serde(crate = "::ploidy_util::serde")]
414                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
415                pub struct NullableOneOf {
416                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
417                    pub value: ::ploidy_util::absent::AbsentOr<::std::string::String>,
418                }
419            }
420        };
421        assert_eq!(actual, expected);
422    }
423
424    #[test]
425    fn test_nullable_schema_value_name_collision() {
426        let doc = Document::from_yaml(indoc::indoc! {"
427            openapi: 3.1.0
428            info:
429              title: Test API
430              version: 1.0.0
431            paths: {}
432            components:
433              schemas:
434                NullableThing:
435                  oneOf:
436                    - type: object
437                      properties:
438                        value:
439                          type: object
440                          properties:
441                            id:
442                              type: string
443                    - type: 'null'
444        "})
445        .unwrap();
446
447        let arena = Arena::new();
448        let spec = Spec::from_doc(&arena, &doc).unwrap();
449        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
450
451        let schema = graph.schema("NullableThing").unwrap();
452        let SchemaTypeView::Container(_, _) = &schema else {
453            panic!("expected container `NullableThing`; got `{schema:?}`");
454        };
455
456        let codegen = CodegenSchemaType::new(&graph, &schema);
457
458        let actual: syn::File = parse_quote!(#codegen);
459        let expected: syn::File = parse_quote! {
460            pub type NullableThing = ::std::option::Option<crate::types::nullable_thing::types::NullableThing>;
461            pub mod types {
462                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
463                #[serde(crate = "::ploidy_util::serde")]
464                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
465                pub struct NullableThing {
466                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
467                    pub value: ::ploidy_util::absent::AbsentOr<crate::types::nullable_thing::types::Value>,
468                }
469                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
470                #[serde(crate = "::ploidy_util::serde")]
471                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
472                pub struct Value {
473                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
474                    pub id: ::ploidy_util::absent::AbsentOr<::std::string::String>,
475                }
476            }
477        };
478        assert_eq!(actual, expected);
479    }
480
481    #[test]
482    fn test_untagged_inline_variants_use_parent_name() {
483        let doc = Document::from_yaml(indoc::indoc! {"
484            openapi: 3.0.0
485            info:
486              title: Test API
487              version: 1.0.0
488            paths: {}
489            components:
490              schemas:
491                Pet:
492                  oneOf:
493                    - type: object
494                      properties:
495                        bark:
496                          type: string
497                    - type: object
498                      properties:
499                        meow:
500                          type: string
501        "})
502        .unwrap();
503
504        let arena = Arena::new();
505        let spec = Spec::from_doc(&arena, &doc).unwrap();
506        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
507
508        let schema = graph.schema("Pet").unwrap();
509        let SchemaTypeView::Untagged(_, _) = &schema else {
510            panic!("expected untagged `Pet`; got `{schema:?}`");
511        };
512
513        let codegen = CodegenSchemaType::new(&graph, &schema);
514
515        let actual: syn::File = parse_quote!(#codegen);
516        let expected: syn::File = parse_quote! {
517            #[derive(Debug, Clone, PartialEq, Eq, Hash, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
518            #[serde(crate = "::ploidy_util::serde", untagged)]
519            #[ploidy(pointer(crate = "::ploidy_util::pointer", untagged))]
520            pub enum Pet {
521                Pet1(crate::types::pet::types::Pet1),
522                Pet2(crate::types::pet::types::Pet2)
523            }
524            pub mod types {
525                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
526                #[serde(crate = "::ploidy_util::serde")]
527                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
528                pub struct Pet1 {
529                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
530                    pub bark: ::ploidy_util::absent::AbsentOr<::std::string::String>,
531                }
532                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
533                #[serde(crate = "::ploidy_util::serde")]
534                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
535                pub struct Pet2 {
536                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
537                    pub meow: ::ploidy_util::absent::AbsentOr<::std::string::String>,
538                }
539            }
540        };
541        assert_eq!(actual, expected);
542    }
543
544    #[test]
545    fn test_any_of_inline_fields_use_parent_name() {
546        let doc = Document::from_yaml(indoc::indoc! {"
547            openapi: 3.0.0
548            info:
549              title: Test API
550              version: 1.0.0
551            paths: {}
552            components:
553              schemas:
554                Pet:
555                  anyOf:
556                    - type: object
557                      properties:
558                        bark:
559                          type: string
560                    - type: object
561                      properties:
562                        meow:
563                          type: string
564        "})
565        .unwrap();
566
567        let arena = Arena::new();
568        let spec = Spec::from_doc(&arena, &doc).unwrap();
569        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
570
571        let schema = graph.schema("Pet").unwrap();
572        let SchemaTypeView::Struct(_, _) = &schema else {
573            panic!("expected struct `Pet`; got `{schema:?}`");
574        };
575
576        let codegen = CodegenSchemaType::new(&graph, &schema);
577
578        let actual: syn::File = parse_quote!(#codegen);
579        let expected: syn::File = parse_quote! {
580            #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
581            #[serde(crate = "::ploidy_util::serde")]
582            #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
583            pub struct Pet {
584                #[serde(flatten, default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
585                #[ploidy(pointer(flatten))]
586                pub pet_1: ::ploidy_util::absent::AbsentOr<crate::types::pet::types::Pet1>,
587                #[serde(flatten, default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
588                #[ploidy(pointer(flatten))]
589                pub pet_2: ::ploidy_util::absent::AbsentOr<crate::types::pet::types::Pet2>,
590            }
591            pub mod types {
592                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
593                #[serde(crate = "::ploidy_util::serde")]
594                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
595                pub struct Pet1 {
596                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
597                    pub bark: ::ploidy_util::absent::AbsentOr<::std::string::String>,
598                }
599                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
600                #[serde(crate = "::ploidy_util::serde")]
601                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
602                pub struct Pet2 {
603                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
604                    pub meow: ::ploidy_util::absent::AbsentOr<::std::string::String>,
605                }
606            }
607        };
608        assert_eq!(actual, expected);
609    }
610
611    #[test]
612    fn test_container_schema_preserves_description() {
613        let doc = Document::from_yaml(indoc::indoc! {"
614            openapi: 3.0.0
615            info:
616              title: Test API
617              version: 1.0.0
618            paths: {}
619            components:
620              schemas:
621                Tags:
622                  description: A list of tags.
623                  type: array
624                  items:
625                    type: string
626        "})
627        .unwrap();
628
629        let arena = Arena::new();
630        let spec = Spec::from_doc(&arena, &doc).unwrap();
631        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
632
633        let schema = graph.schema("Tags").unwrap();
634        let SchemaTypeView::Container(_, _) = &schema else {
635            panic!("expected container `Tags`; got `{schema:?}`");
636        };
637
638        let codegen = CodegenSchemaType::new(&graph, &schema);
639
640        let actual: syn::File = parse_quote!(#codegen);
641        let expected: syn::File = parse_quote! {
642            #[doc = " A list of tags."]
643            pub type Tags = ::std::vec::Vec<::std::string::String>;
644        };
645        assert_eq!(actual, expected);
646    }
647
648    #[test]
649    fn test_case_colliding_fields_uniquify_inline_type_names() {
650        // `fooBar` and `foo_bar` both normalize to `foo_bar` in Rust,
651        // so the field names — and their inline type names — must be
652        // uniquified to avoid collisions.
653        let doc = Document::from_yaml(indoc::indoc! {"
654            openapi: 3.0.0
655            info:
656              title: Test API
657              version: 1.0.0
658            paths: {}
659            components:
660              schemas:
661                Qux:
662                  type: object
663                  properties:
664                    fooBar:
665                      type: array
666                      items:
667                        type: object
668                        properties:
669                          zoom:
670                            type: string
671                    foo_bar:
672                      type: array
673                      items:
674                        type: object
675                        properties:
676                          blagh:
677                            type: string
678        "})
679        .unwrap();
680
681        let arena = Arena::new();
682        let spec = Spec::from_doc(&arena, &doc).unwrap();
683        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
684
685        let schema = graph.schema("Qux").unwrap();
686        let SchemaTypeView::Struct(_, _) = &schema else {
687            panic!("expected struct `Qux`; got `{schema:?}`");
688        };
689
690        let codegen = CodegenSchemaType::new(&graph, &schema);
691
692        let actual: syn::File = parse_quote!(#codegen);
693        let expected: syn::File = parse_quote! {
694            #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
695            #[serde(crate = "::ploidy_util::serde")]
696            #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
697            pub struct Qux {
698                #[serde(rename = "fooBar", default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
699                #[ploidy(pointer(rename = "fooBar"))]
700                pub foo_bar: ::ploidy_util::absent::AbsentOr<::std::vec::Vec<crate::types::qux::types::FooBarItem>>,
701                #[serde(rename = "foo_bar", default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
702                #[ploidy(pointer(rename = "foo_bar"))]
703                pub foo_bar_2: ::ploidy_util::absent::AbsentOr<::std::vec::Vec<crate::types::qux::types::FooBar2Item>>,
704            }
705            pub mod types {
706                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
707                #[serde(crate = "::ploidy_util::serde")]
708                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
709                pub struct FooBar2Item {
710                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
711                    pub blagh: ::ploidy_util::absent::AbsentOr<::std::string::String>,
712                }
713                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
714                #[serde(crate = "::ploidy_util::serde")]
715                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
716                pub struct FooBarItem {
717                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
718                    pub zoom: ::ploidy_util::absent::AbsentOr<::std::string::String>,
719                }
720            }
721        };
722        assert_eq!(actual, expected);
723    }
724
725    #[test]
726    fn test_colliding_inline_paths_uniquify_inline_type_names() {
727        let doc = Document::from_yaml(indoc::indoc! {"
728            openapi: 3.0.0
729            info:
730              title: Collision API
731              version: 1.0.0
732            paths: {}
733            components:
734              schemas:
735                Qux:
736                  type: object
737                  properties:
738                    fooItem:
739                      type: object
740                      properties:
741                        direct:
742                          type: string
743                    foo:
744                      type: array
745                      items:
746                        type: object
747                        properties:
748                          nested:
749                            type: string
750        "})
751        .unwrap();
752
753        let arena = Arena::new();
754        let spec = Spec::from_doc(&arena, &doc).unwrap();
755        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
756
757        let schema = graph.schema("Qux").unwrap();
758        let SchemaTypeView::Struct(_, _) = &schema else {
759            panic!("expected struct `Qux`; got `{schema:?}`");
760        };
761
762        let codegen = CodegenSchemaType::new(&graph, &schema);
763
764        let actual: syn::File = parse_quote!(#codegen);
765        let expected: syn::File = parse_quote! {
766            #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
767            #[serde(crate = "::ploidy_util::serde")]
768            #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
769            pub struct Qux {
770                #[serde(rename = "fooItem", default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
771                #[ploidy(pointer(rename = "fooItem"))]
772                pub foo_item: ::ploidy_util::absent::AbsentOr<crate::types::qux::types::FooItem>,
773                #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
774                pub foo: ::ploidy_util::absent::AbsentOr<::std::vec::Vec<crate::types::qux::types::FooItem2>>,
775            }
776            pub mod types {
777                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
778                #[serde(crate = "::ploidy_util::serde")]
779                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
780                pub struct FooItem {
781                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
782                    pub direct: ::ploidy_util::absent::AbsentOr<::std::string::String>,
783                }
784                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
785                #[serde(crate = "::ploidy_util::serde")]
786                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
787                pub struct FooItem2 {
788                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
789                    pub nested: ::ploidy_util::absent::AbsentOr<::std::string::String>,
790                }
791            }
792        };
793        assert_eq!(actual, expected);
794    }
795
796    #[test]
797    fn test_tagged_common_inline_field_codegen() {
798        // `metadata` is a common field on the tagged union itself.
799        // Its inline object type is reached through a `Field` edge
800        // whose parent is a tagged view, not a struct view.
801        let doc = Document::from_yaml(indoc::indoc! {"
802            openapi: 3.0.0
803            info:
804              title: Test API
805              version: 1.0.0
806            paths: {}
807            components:
808              schemas:
809                Dog:
810                  type: object
811                  properties:
812                    kind:
813                      type: string
814                    bark:
815                      type: string
816                Pet:
817                  oneOf:
818                    - $ref: '#/components/schemas/Dog'
819                  discriminator:
820                    propertyName: kind
821                    mapping:
822                      dog: '#/components/schemas/Dog'
823                  properties:
824                    metadata:
825                      type: object
826                      properties:
827                        source:
828                          type: string
829        "})
830        .unwrap();
831
832        let arena = Arena::new();
833        let spec = Spec::from_doc(&arena, &doc).unwrap();
834        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
835
836        let schema = graph.schema("Pet").unwrap();
837        let SchemaTypeView::Tagged(_, _) = &schema else {
838            panic!("expected tagged `Pet`; got `{schema:?}`");
839        };
840
841        let codegen = CodegenSchemaType::new(&graph, &schema);
842
843        let actual: syn::File = parse_quote!(#codegen);
844        let expected: syn::File = parse_quote! {
845            #[derive(Debug, Clone, PartialEq, Eq, Hash, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
846            #[serde(crate = "::ploidy_util::serde", tag = "kind")]
847            #[ploidy(pointer(crate = "::ploidy_util::pointer", tag = "kind"))]
848            pub enum Pet {
849                #[serde(rename = "dog")]
850                #[ploidy(pointer(rename = "dog"))]
851                Dog(crate::types::Dog),
852            }
853
854            impl ::std::convert::From<crate::types::Dog> for Pet {
855                fn from(value: crate::types::Dog) -> Self {
856                    Self::Dog(value)
857                }
858            }
859
860            pub mod types {
861                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
862                #[serde(crate = "::ploidy_util::serde")]
863                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
864                pub struct Metadata {
865                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
866                    pub source: ::ploidy_util::absent::AbsentOr<::std::string::String>,
867                }
868            }
869        };
870        assert_eq!(actual, expected);
871    }
872}