1use ploidy_core::codegen::IntoCode;
2use ploidy_core::ir::{ContainerView, SchemaTypeView};
3use proc_macro2::TokenStream;
4use quote::{ToTokens, TokenStreamExt, quote};
5
6use super::{
7 doc_attrs, enum_::CodegenEnum, inlines::CodegenInlines, naming::CodegenTypeName,
8 primitive::CodegenPrimitive, ref_::CodegenRef, struct_::CodegenStruct, tagged::CodegenTagged,
9 untagged::CodegenUntagged,
10};
11
12#[derive(Debug)]
14pub struct CodegenSchemaType<'a> {
15 ty: &'a SchemaTypeView<'a>,
16}
17
18impl<'a> CodegenSchemaType<'a> {
19 pub fn new(ty: &'a SchemaTypeView<'a>) -> Self {
20 Self { ty }
21 }
22}
23
24impl ToTokens for CodegenSchemaType<'_> {
25 fn to_tokens(&self, tokens: &mut TokenStream) {
26 let name = CodegenTypeName::Schema(self.ty);
27 let ty = match self.ty {
28 SchemaTypeView::Struct(_, view) => CodegenStruct::new(name, view).into_token_stream(),
29 SchemaTypeView::Enum(_, view) => CodegenEnum::new(name, view).into_token_stream(),
30 SchemaTypeView::Tagged(_, view) => CodegenTagged::new(name, view).into_token_stream(),
31 SchemaTypeView::Untagged(_, view) => {
32 CodegenUntagged::new(name, view).into_token_stream()
33 }
34 SchemaTypeView::Container(_, ContainerView::Array(inner)) => {
35 let doc_attrs = inner.description().map(doc_attrs);
36 let inner_ty = inner.ty();
37 let inner_ref = CodegenRef::new(&inner_ty);
38 quote! {
39 #doc_attrs
40 pub type #name = ::std::vec::Vec<#inner_ref>;
41 }
42 }
43 SchemaTypeView::Container(_, ContainerView::Map(inner)) => {
44 let doc_attrs = inner.description().map(doc_attrs);
45 let inner_ty = inner.ty();
46 let inner_ref = CodegenRef::new(&inner_ty);
47 quote! {
48 #doc_attrs
49 pub type #name = ::std::collections::BTreeMap<::std::string::String, #inner_ref>;
50 }
51 }
52 SchemaTypeView::Container(_, ContainerView::Optional(inner)) => {
53 let doc_attrs = inner.description().map(doc_attrs);
54 let inner_ty = inner.ty();
55 let inner_ref = CodegenRef::new(&inner_ty);
56 quote! {
57 #doc_attrs
58 pub type #name = ::std::option::Option<#inner_ref>;
59 }
60 }
61 SchemaTypeView::Primitive(_, view) => {
62 let primitive = CodegenPrimitive::new(view);
63 quote! {
64 pub type #name = #primitive;
65 }
66 }
67 SchemaTypeView::Any(_, _) => {
68 quote! {
69 pub type #name = ::ploidy_util::serde_json::Value;
70 }
71 }
72 };
73 let inlines = CodegenInlines::Schema(self.ty);
74 tokens.append_all(quote! {
75 #ty
76 #inlines
77 });
78 }
79}
80
81impl IntoCode for CodegenSchemaType<'_> {
82 type Code = (String, TokenStream);
83
84 fn into_code(self) -> Self::Code {
85 let name = CodegenTypeName::Schema(self.ty);
86 (
87 format!("src/types/{}.rs", name.into_module_name().display()),
88 self.into_token_stream(),
89 )
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 use ploidy_core::{
98 arena::Arena,
99 ir::{RawGraph, SchemaTypeView, Spec},
100 parse::Document,
101 };
102 use pretty_assertions::assert_eq;
103 use syn::parse_quote;
104
105 use crate::CodegenGraph;
106
107 #[test]
108 fn test_schema_inline_types_order() {
109 let doc = Document::from_yaml(indoc::indoc! {"
112 openapi: 3.0.0
113 info:
114 title: Test API
115 version: 1.0.0
116 paths: {}
117 components:
118 schemas:
119 Container:
120 type: object
121 properties:
122 zebra:
123 type: object
124 properties:
125 name:
126 type: string
127 mango:
128 type: object
129 properties:
130 name:
131 type: string
132 apple:
133 type: object
134 properties:
135 name:
136 type: string
137 "})
138 .unwrap();
139
140 let arena = Arena::new();
141 let spec = Spec::from_doc(&arena, &doc).unwrap();
142 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
143
144 let schema = graph.schemas().find(|s| s.name() == "Container");
145 let Some(schema @ SchemaTypeView::Struct(_, _)) = &schema else {
146 panic!("expected struct `Container`; got `{schema:?}`");
147 };
148
149 let codegen = CodegenSchemaType::new(schema);
150
151 let actual: syn::File = parse_quote!(#codegen);
152 let expected: syn::File = parse_quote! {
156 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
157 #[serde(crate = "::ploidy_util::serde")]
158 pub struct Container {
159 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
160 pub zebra: ::ploidy_util::absent::AbsentOr<crate::types::container::types::Zebra>,
161 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
162 pub mango: ::ploidy_util::absent::AbsentOr<crate::types::container::types::Mango>,
163 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
164 pub apple: ::ploidy_util::absent::AbsentOr<crate::types::container::types::Apple>,
165 }
166 pub mod types {
167 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
168 #[serde(crate = "::ploidy_util::serde")]
169 pub struct Apple {
170 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
171 pub name: ::ploidy_util::absent::AbsentOr<::std::string::String>,
172 }
173 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
174 #[serde(crate = "::ploidy_util::serde")]
175 pub struct Mango {
176 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
177 pub name: ::ploidy_util::absent::AbsentOr<::std::string::String>,
178 }
179 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
180 #[serde(crate = "::ploidy_util::serde")]
181 pub struct Zebra {
182 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
183 pub name: ::ploidy_util::absent::AbsentOr<::std::string::String>,
184 }
185 }
186 };
187 assert_eq!(actual, expected);
188 }
189
190 #[test]
191 fn test_container_schema_emits_type_alias_with_inline_types() {
192 let doc = Document::from_yaml(indoc::indoc! {"
195 openapi: 3.0.0
196 info:
197 title: Test API
198 version: 1.0.0
199 paths: {}
200 components:
201 schemas:
202 InvalidParameters:
203 type: array
204 items:
205 type: object
206 required:
207 - name
208 - reason
209 properties:
210 name:
211 type: string
212 reason:
213 type: string
214 "})
215 .unwrap();
216
217 let arena = Arena::new();
218 let spec = Spec::from_doc(&arena, &doc).unwrap();
219 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
220
221 let schema = graph.schemas().find(|s| s.name() == "InvalidParameters");
222 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
223 panic!("expected container `InvalidParameters`; got `{schema:?}`");
224 };
225
226 let codegen = CodegenSchemaType::new(schema);
227
228 let actual: syn::File = parse_quote!(#codegen);
229 let expected: syn::File = parse_quote! {
230 pub type InvalidParameters = ::std::vec::Vec<crate::types::invalid_parameters::types::Item>;
231 pub mod types {
232 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
233 #[serde(crate = "::ploidy_util::serde")]
234 pub struct Item {
235 pub name: ::std::string::String,
236 pub reason: ::std::string::String,
237 }
238 }
239 };
240 assert_eq!(actual, expected);
241 }
242
243 #[test]
244 fn test_container_schema_emits_type_alias_without_inline_types() {
245 let doc = Document::from_yaml(indoc::indoc! {"
247 openapi: 3.0.0
248 info:
249 title: Test API
250 version: 1.0.0
251 paths: {}
252 components:
253 schemas:
254 Tags:
255 type: array
256 items:
257 type: string
258 "})
259 .unwrap();
260
261 let arena = Arena::new();
262 let spec = Spec::from_doc(&arena, &doc).unwrap();
263 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
264
265 let schema = graph.schemas().find(|s| s.name() == "Tags");
266 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
267 panic!("expected container `Tags`; got `{schema:?}`");
268 };
269
270 let codegen = CodegenSchemaType::new(schema);
271
272 let actual: syn::File = parse_quote!(#codegen);
273 let expected: syn::File = parse_quote! {
274 pub type Tags = ::std::vec::Vec<::std::string::String>;
275 };
276 assert_eq!(actual, expected);
277 }
278
279 #[test]
280 fn test_container_schema_map_emits_type_alias() {
281 let doc = Document::from_yaml(indoc::indoc! {"
282 openapi: 3.0.0
283 info:
284 title: Test API
285 version: 1.0.0
286 paths: {}
287 components:
288 schemas:
289 Metadata:
290 type: object
291 additionalProperties:
292 type: string
293 "})
294 .unwrap();
295
296 let arena = Arena::new();
297 let spec = Spec::from_doc(&arena, &doc).unwrap();
298 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
299
300 let schema = graph.schemas().find(|s| s.name() == "Metadata");
301 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
302 panic!("expected container `Metadata`; got `{schema:?}`");
303 };
304
305 let codegen = CodegenSchemaType::new(schema);
306
307 let actual: syn::File = parse_quote!(#codegen);
308 let expected: syn::File = parse_quote! {
309 pub type Metadata = ::std::collections::BTreeMap<::std::string::String, ::std::string::String>;
310 };
311 assert_eq!(actual, expected);
312 }
313
314 #[test]
315 fn test_container_nullable_schema() {
316 let doc = Document::from_yaml(indoc::indoc! {"
317 openapi: 3.1.0
318 info:
319 title: Test API
320 version: 1.0.0
321 paths: {}
322 components:
323 schemas:
324 NullableString:
325 type: [string, 'null']
326 NullableArray:
327 type: [array, 'null']
328 items:
329 type: string
330 NullableMap:
331 type: [object, 'null']
332 additionalProperties:
333 type: string
334 NullableOneOf:
335 oneOf:
336 - type: object
337 properties:
338 value:
339 type: string
340 - type: 'null'
341 "})
342 .unwrap();
343
344 let arena = Arena::new();
345 let spec = Spec::from_doc(&arena, &doc).unwrap();
346 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
347
348 let schema = graph.schemas().find(|s| s.name() == "NullableString");
350 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
351 panic!("expected container `NullableString`; got `{schema:?}`");
352 };
353 let codegen = CodegenSchemaType::new(schema);
354 let actual: syn::File = parse_quote!(#codegen);
355 let expected: syn::File = parse_quote! {
356 pub type NullableString = ::std::option::Option<::std::string::String>;
357 };
358 assert_eq!(actual, expected);
359
360 let schema = graph.schemas().find(|s| s.name() == "NullableArray");
362 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
363 panic!("expected container `NullableArray`; got `{schema:?}`");
364 };
365 let codegen = CodegenSchemaType::new(schema);
366 let actual: syn::File = parse_quote!(#codegen);
367 let expected: syn::File = parse_quote! {
368 pub type NullableArray = ::std::option::Option<::std::vec::Vec<::std::string::String>>;
369 };
370 assert_eq!(actual, expected);
371
372 let schema = graph.schemas().find(|s| s.name() == "NullableMap");
375 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
376 panic!("expected container `NullableMap`; got `{schema:?}`");
377 };
378 let codegen = CodegenSchemaType::new(schema);
379 let actual: syn::File = parse_quote!(#codegen);
380 let expected: syn::File = parse_quote! {
381 pub type NullableMap = ::std::option::Option<::std::collections::BTreeMap<::std::string::String, ::std::string::String>>;
382 };
383 assert_eq!(actual, expected);
384
385 let schema = graph.schemas().find(|s| s.name() == "NullableOneOf");
388 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
389 panic!("expected container `NullableOneOf`; got `{schema:?}`");
390 };
391 let codegen = CodegenSchemaType::new(schema);
392 let actual: syn::File = parse_quote!(#codegen);
393 let expected: syn::File = parse_quote! {
394 pub type NullableOneOf = ::std::option::Option<crate::types::nullable_one_of::types::V1>;
395 pub mod types {
396 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
397 #[serde(crate = "::ploidy_util::serde")]
398 pub struct V1 {
399 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
400 pub value: ::ploidy_util::absent::AbsentOr<::std::string::String>,
401 }
402 }
403 };
404 assert_eq!(actual, expected);
405 }
406
407 #[test]
408 fn test_container_schema_preserves_description() {
409 let doc = Document::from_yaml(indoc::indoc! {"
410 openapi: 3.0.0
411 info:
412 title: Test API
413 version: 1.0.0
414 paths: {}
415 components:
416 schemas:
417 Tags:
418 description: A list of tags.
419 type: array
420 items:
421 type: string
422 "})
423 .unwrap();
424
425 let arena = Arena::new();
426 let spec = Spec::from_doc(&arena, &doc).unwrap();
427 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
428
429 let schema = graph.schemas().find(|s| s.name() == "Tags");
430 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
431 panic!("expected container `Tags`; got `{schema:?}`");
432 };
433
434 let codegen = CodegenSchemaType::new(schema);
435
436 let actual: syn::File = parse_quote!(#codegen);
437 let expected: syn::File = parse_quote! {
438 #[doc = "A list of tags."]
439 pub type Tags = ::std::vec::Vec<::std::string::String>;
440 };
441 assert_eq!(actual, expected);
442 }
443}