ploidy_codegen_rust/
schema.rs

1use itertools::Itertools;
2use ploidy_core::{
3    codegen::IntoCode,
4    ir::{InlineIrTypeView, SchemaIrTypeView, View},
5};
6use proc_macro2::TokenStream;
7use quote::{ToTokens, TokenStreamExt, quote};
8
9use super::{
10    enum_::CodegenEnum,
11    naming::{CodegenIdent, CodegenIdentUsage, CodegenTypeName},
12    struct_::CodegenStruct,
13    tagged::CodegenTagged,
14    untagged::CodegenUntagged,
15};
16
17/// Generates a module for a named schema type.
18#[derive(Debug)]
19pub struct CodegenSchemaType<'a> {
20    ty: &'a SchemaIrTypeView<'a>,
21}
22
23impl<'a> CodegenSchemaType<'a> {
24    pub fn new(ty: &'a SchemaIrTypeView<'a>) -> Self {
25        Self { ty }
26    }
27}
28
29impl ToTokens for CodegenSchemaType<'_> {
30    fn to_tokens(&self, tokens: &mut TokenStream) {
31        let name = CodegenTypeName::Schema(self.ty);
32        let code = match self.ty {
33            SchemaIrTypeView::Struct(_, view) => CodegenStruct::new(name, view).into_token_stream(),
34            SchemaIrTypeView::Enum(_, view) => CodegenEnum::new(name, view).into_token_stream(),
35            SchemaIrTypeView::Tagged(_, view) => CodegenTagged::new(name, view).into_token_stream(),
36            SchemaIrTypeView::Untagged(_, view) => {
37                CodegenUntagged::new(name, view).into_token_stream()
38            }
39        };
40        let mut inlines = self.ty.inlines().collect_vec();
41        inlines.sort_by(|a, b| {
42            CodegenTypeName::Inline(a)
43                .into_sort_key()
44                .cmp(&CodegenTypeName::Inline(b).into_sort_key())
45        });
46        let mut inlines = inlines.into_iter().map(|view| {
47            let name = CodegenTypeName::Inline(&view);
48            match &view {
49                InlineIrTypeView::Enum(_, view) => CodegenEnum::new(name, view).into_token_stream(),
50                InlineIrTypeView::Struct(_, view) => {
51                    CodegenStruct::new(name, view).into_token_stream()
52                }
53                InlineIrTypeView::Tagged(_, view) => {
54                    CodegenTagged::new(name, view).into_token_stream()
55                }
56                InlineIrTypeView::Untagged(_, view) => {
57                    CodegenUntagged::new(name, view).into_token_stream()
58                }
59            }
60        });
61        let fields_module = inlines.next().map(|head| {
62            quote! {
63                pub mod types {
64                    #head
65                    #(#inlines)*
66                }
67            }
68        });
69        tokens.append_all(quote! {
70            #code
71            #fields_module
72        });
73    }
74}
75
76impl IntoCode for CodegenSchemaType<'_> {
77    type Code = (String, TokenStream);
78
79    fn into_code(self) -> Self::Code {
80        let ident = self.ty.extensions().get::<CodegenIdent>().unwrap();
81        let usage = CodegenIdentUsage::Module(&ident);
82        (format!("src/types/{usage}.rs"), self.into_token_stream())
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    use ploidy_core::{
91        ir::{IrGraph, IrSpec, SchemaIrTypeView},
92        parse::Document,
93    };
94    use pretty_assertions::assert_eq;
95    use syn::parse_quote;
96
97    use crate::CodegenGraph;
98
99    #[test]
100    fn test_schema_inline_types_order() {
101        // Inline types are defined in reverse alphabetical order (Zebra, Mango, Apple),
102        // to verify that they're sorted in the output.
103        let doc = Document::from_yaml(indoc::indoc! {"
104            openapi: 3.0.0
105            info:
106              title: Test API
107              version: 1.0.0
108            paths: {}
109            components:
110              schemas:
111                Container:
112                  type: object
113                  properties:
114                    zebra:
115                      type: object
116                      properties:
117                        name:
118                          type: string
119                    mango:
120                      type: object
121                      properties:
122                        name:
123                          type: string
124                    apple:
125                      type: object
126                      properties:
127                        name:
128                          type: string
129        "})
130        .unwrap();
131
132        let spec = IrSpec::from_doc(&doc).unwrap();
133        let ir = IrGraph::new(&spec);
134        let graph = CodegenGraph::new(ir);
135
136        let schema = graph.schemas().find(|s| s.name() == "Container");
137        let Some(schema @ SchemaIrTypeView::Struct(_, _)) = &schema else {
138            panic!("expected struct `Container`; got `{schema:?}`");
139        };
140
141        let codegen = CodegenSchemaType::new(schema);
142
143        let actual: syn::File = parse_quote!(#codegen);
144        // The struct fields remain in their original order (`zebra`, `mango`, `apple`),
145        // but the inline types in `mod types` should be sorted alphabetically
146        // (`Apple`, `Mango`, `Zebra`).
147        let expected: syn::File = parse_quote! {
148            #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::serde::Serialize, ::serde::Deserialize)]
149            pub struct Container {
150                #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent",)]
151                pub zebra: ::ploidy_util::absent::AbsentOr<crate::types::container::types::Zebra>,
152                #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent",)]
153                pub mango: ::ploidy_util::absent::AbsentOr<crate::types::container::types::Mango>,
154                #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent",)]
155                pub apple: ::ploidy_util::absent::AbsentOr<crate::types::container::types::Apple>,
156            }
157            pub mod types {
158                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::serde::Serialize, ::serde::Deserialize)]
159                pub struct Apple {
160                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent",)]
161                    pub name: ::ploidy_util::absent::AbsentOr<::std::string::String>,
162                }
163                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::serde::Serialize, ::serde::Deserialize)]
164                pub struct Mango {
165                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent",)]
166                    pub name: ::ploidy_util::absent::AbsentOr<::std::string::String>,
167                }
168                #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::serde::Serialize, ::serde::Deserialize)]
169                pub struct Zebra {
170                    #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent",)]
171                    pub name: ::ploidy_util::absent::AbsentOr<::std::string::String>,
172                }
173            }
174        };
175        assert_eq!(actual, expected);
176    }
177}