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#[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 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 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}