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::Value>;
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 Value {
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::Value>;
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 Value {
466                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
467                    pub value: ::ploidy_util::absent::AbsentOr<crate::types::nullable_thing::types::Value2>,
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 Value2 {
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_container_schema_preserves_description() {
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                Tags:
492                  description: A list of tags.
493                  type: array
494                  items:
495                    type: string
496        "})
497        .unwrap();
498
499        let arena = Arena::new();
500        let spec = Spec::from_doc(&arena, &doc).unwrap();
501        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
502
503        let schema = graph.schema("Tags").unwrap();
504        let SchemaTypeView::Container(_, _) = &schema else {
505            panic!("expected container `Tags`; got `{schema:?}`");
506        };
507
508        let codegen = CodegenSchemaType::new(&graph, &schema);
509
510        let actual: syn::File = parse_quote!(#codegen);
511        let expected: syn::File = parse_quote! {
512            #[doc = "A list of tags."]
513            pub type Tags = ::std::vec::Vec<::std::string::String>;
514        };
515        assert_eq!(actual, expected);
516    }
517
518    #[test]
519    fn test_case_colliding_fields_uniquify_inline_type_names() {
520        // `fooBar` and `foo_bar` both normalize to `foo_bar` in Rust,
521        // so the field names — and their inline type names — must be
522        // uniquified to avoid collisions.
523        let doc = Document::from_yaml(indoc::indoc! {"
524            openapi: 3.0.0
525            info:
526              title: Test API
527              version: 1.0.0
528            paths: {}
529            components:
530              schemas:
531                Qux:
532                  type: object
533                  properties:
534                    fooBar:
535                      type: array
536                      items:
537                        type: object
538                        properties:
539                          zoom:
540                            type: string
541                    foo_bar:
542                      type: array
543                      items:
544                        type: object
545                        properties:
546                          blagh:
547                            type: string
548        "})
549        .unwrap();
550
551        let arena = Arena::new();
552        let spec = Spec::from_doc(&arena, &doc).unwrap();
553        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
554
555        let schema = graph.schema("Qux").unwrap();
556        let SchemaTypeView::Struct(_, _) = &schema else {
557            panic!("expected struct `Qux`; got `{schema:?}`");
558        };
559
560        let codegen = CodegenSchemaType::new(&graph, &schema);
561
562        let actual: syn::File = parse_quote!(#codegen);
563        let expected: syn::File = parse_quote! {
564            #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
565            #[serde(crate = "::ploidy_util::serde")]
566            #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
567            pub struct Qux {
568                #[serde(rename = "fooBar", default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
569                #[ploidy(pointer(rename = "fooBar"))]
570                pub foo_bar: ::ploidy_util::absent::AbsentOr<::std::vec::Vec<crate::types::qux::types::FooBarItem>>,
571                #[serde(rename = "foo_bar", default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
572                #[ploidy(pointer(rename = "foo_bar"))]
573                pub foo_bar2: ::ploidy_util::absent::AbsentOr<::std::vec::Vec<crate::types::qux::types::FooBar2Item>>,
574            }
575            pub mod types {
576                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
577                #[serde(crate = "::ploidy_util::serde")]
578                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
579                pub struct FooBar2Item {
580                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
581                    pub blagh: ::ploidy_util::absent::AbsentOr<::std::string::String>,
582                }
583                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
584                #[serde(crate = "::ploidy_util::serde")]
585                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
586                pub struct FooBarItem {
587                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
588                    pub zoom: ::ploidy_util::absent::AbsentOr<::std::string::String>,
589                }
590            }
591        };
592        assert_eq!(actual, expected);
593    }
594
595    #[test]
596    fn test_colliding_inline_paths_uniquify_inline_type_names() {
597        let doc = Document::from_yaml(indoc::indoc! {"
598            openapi: 3.0.0
599            info:
600              title: Collision API
601              version: 1.0.0
602            paths: {}
603            components:
604              schemas:
605                Qux:
606                  type: object
607                  properties:
608                    fooItem:
609                      type: object
610                      properties:
611                        direct:
612                          type: string
613                    foo:
614                      type: array
615                      items:
616                        type: object
617                        properties:
618                          nested:
619                            type: string
620        "})
621        .unwrap();
622
623        let arena = Arena::new();
624        let spec = Spec::from_doc(&arena, &doc).unwrap();
625        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
626
627        let schema = graph.schema("Qux").unwrap();
628        let SchemaTypeView::Struct(_, _) = &schema else {
629            panic!("expected struct `Qux`; got `{schema:?}`");
630        };
631
632        let codegen = CodegenSchemaType::new(&graph, &schema);
633
634        let actual: syn::File = parse_quote!(#codegen);
635        let expected: syn::File = parse_quote! {
636            #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
637            #[serde(crate = "::ploidy_util::serde")]
638            #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
639            pub struct Qux {
640                #[serde(rename = "fooItem", default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
641                #[ploidy(pointer(rename = "fooItem"))]
642                pub foo_item: ::ploidy_util::absent::AbsentOr<crate::types::qux::types::FooItem>,
643                #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
644                pub foo: ::ploidy_util::absent::AbsentOr<::std::vec::Vec<crate::types::qux::types::FooItem2>>,
645            }
646            pub mod types {
647                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
648                #[serde(crate = "::ploidy_util::serde")]
649                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
650                pub struct FooItem {
651                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
652                    pub direct: ::ploidy_util::absent::AbsentOr<::std::string::String>,
653                }
654                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
655                #[serde(crate = "::ploidy_util::serde")]
656                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
657                pub struct FooItem2 {
658                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
659                    pub nested: ::ploidy_util::absent::AbsentOr<::std::string::String>,
660                }
661            }
662        };
663        assert_eq!(actual, expected);
664    }
665
666    #[test]
667    fn test_tagged_common_inline_field_codegen() {
668        // `metadata` is a common field on the tagged union itself.
669        // Its inline object type is reached through a `Field` edge
670        // whose parent is a tagged view, not a struct view.
671        let doc = Document::from_yaml(indoc::indoc! {"
672            openapi: 3.0.0
673            info:
674              title: Test API
675              version: 1.0.0
676            paths: {}
677            components:
678              schemas:
679                Dog:
680                  type: object
681                  properties:
682                    kind:
683                      type: string
684                    bark:
685                      type: string
686                Pet:
687                  oneOf:
688                    - $ref: '#/components/schemas/Dog'
689                  discriminator:
690                    propertyName: kind
691                    mapping:
692                      dog: '#/components/schemas/Dog'
693                  properties:
694                    metadata:
695                      type: object
696                      properties:
697                        source:
698                          type: string
699        "})
700        .unwrap();
701
702        let arena = Arena::new();
703        let spec = Spec::from_doc(&arena, &doc).unwrap();
704        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
705
706        let schema = graph.schema("Pet").unwrap();
707        let SchemaTypeView::Tagged(_, _) = &schema else {
708            panic!("expected tagged `Pet`; got `{schema:?}`");
709        };
710
711        let codegen = CodegenSchemaType::new(&graph, &schema);
712
713        let actual: syn::File = parse_quote!(#codegen);
714        let expected: syn::File = parse_quote! {
715            #[derive(Debug, Clone, PartialEq, Eq, Hash, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
716            #[serde(crate = "::ploidy_util::serde", tag = "kind")]
717            #[ploidy(pointer(crate = "::ploidy_util::pointer", tag = "kind"))]
718            pub enum Pet {
719                #[serde(rename = "dog")]
720                #[ploidy(pointer(rename = "dog"))]
721                Dog(crate::types::Dog),
722            }
723
724            impl ::std::convert::From<crate::types::Dog> for Pet {
725                fn from(value: crate::types::Dog) -> Self {
726                    Self::Dog(value)
727                }
728            }
729
730            pub mod types {
731                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
732                #[serde(crate = "::ploidy_util::serde")]
733                #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
734                pub struct Metadata {
735                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
736                    pub source: ::ploidy_util::absent::AbsentOr<::std::string::String>,
737                }
738            }
739        };
740        assert_eq!(actual, expected);
741    }
742}