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, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
157 #[serde(crate = "::ploidy_util::serde")]
158 #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
159 pub struct Container {
160 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
161 pub zebra: ::ploidy_util::absent::AbsentOr<crate::types::container::types::Zebra>,
162 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
163 pub mango: ::ploidy_util::absent::AbsentOr<crate::types::container::types::Mango>,
164 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
165 pub apple: ::ploidy_util::absent::AbsentOr<crate::types::container::types::Apple>,
166 }
167 pub mod types {
168 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
169 #[serde(crate = "::ploidy_util::serde")]
170 #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
171 pub struct Apple {
172 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
173 pub name: ::ploidy_util::absent::AbsentOr<::std::string::String>,
174 }
175 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
176 #[serde(crate = "::ploidy_util::serde")]
177 #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
178 pub struct Mango {
179 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
180 pub name: ::ploidy_util::absent::AbsentOr<::std::string::String>,
181 }
182 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
183 #[serde(crate = "::ploidy_util::serde")]
184 #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
185 pub struct Zebra {
186 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
187 pub name: ::ploidy_util::absent::AbsentOr<::std::string::String>,
188 }
189 }
190 };
191 assert_eq!(actual, expected);
192 }
193
194 #[test]
195 fn test_container_schema_emits_type_alias_with_inline_types() {
196 let doc = Document::from_yaml(indoc::indoc! {"
199 openapi: 3.0.0
200 info:
201 title: Test API
202 version: 1.0.0
203 paths: {}
204 components:
205 schemas:
206 InvalidParameters:
207 type: array
208 items:
209 type: object
210 required:
211 - name
212 - reason
213 properties:
214 name:
215 type: string
216 reason:
217 type: string
218 "})
219 .unwrap();
220
221 let arena = Arena::new();
222 let spec = Spec::from_doc(&arena, &doc).unwrap();
223 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
224
225 let schema = graph.schemas().find(|s| s.name() == "InvalidParameters");
226 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
227 panic!("expected container `InvalidParameters`; got `{schema:?}`");
228 };
229
230 let codegen = CodegenSchemaType::new(schema);
231
232 let actual: syn::File = parse_quote!(#codegen);
233 let expected: syn::File = parse_quote! {
234 pub type InvalidParameters = ::std::vec::Vec<crate::types::invalid_parameters::types::Item>;
235 pub mod types {
236 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
237 #[serde(crate = "::ploidy_util::serde")]
238 #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
239 pub struct Item {
240 pub name: ::std::string::String,
241 pub reason: ::std::string::String,
242 }
243 }
244 };
245 assert_eq!(actual, expected);
246 }
247
248 #[test]
249 fn test_container_schema_emits_type_alias_without_inline_types() {
250 let doc = Document::from_yaml(indoc::indoc! {"
252 openapi: 3.0.0
253 info:
254 title: Test API
255 version: 1.0.0
256 paths: {}
257 components:
258 schemas:
259 Tags:
260 type: array
261 items:
262 type: string
263 "})
264 .unwrap();
265
266 let arena = Arena::new();
267 let spec = Spec::from_doc(&arena, &doc).unwrap();
268 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
269
270 let schema = graph.schemas().find(|s| s.name() == "Tags");
271 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
272 panic!("expected container `Tags`; got `{schema:?}`");
273 };
274
275 let codegen = CodegenSchemaType::new(schema);
276
277 let actual: syn::File = parse_quote!(#codegen);
278 let expected: syn::File = parse_quote! {
279 pub type Tags = ::std::vec::Vec<::std::string::String>;
280 };
281 assert_eq!(actual, expected);
282 }
283
284 #[test]
285 fn test_container_schema_map_emits_type_alias() {
286 let doc = Document::from_yaml(indoc::indoc! {"
287 openapi: 3.0.0
288 info:
289 title: Test API
290 version: 1.0.0
291 paths: {}
292 components:
293 schemas:
294 Metadata:
295 type: object
296 additionalProperties:
297 type: string
298 "})
299 .unwrap();
300
301 let arena = Arena::new();
302 let spec = Spec::from_doc(&arena, &doc).unwrap();
303 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
304
305 let schema = graph.schemas().find(|s| s.name() == "Metadata");
306 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
307 panic!("expected container `Metadata`; got `{schema:?}`");
308 };
309
310 let codegen = CodegenSchemaType::new(schema);
311
312 let actual: syn::File = parse_quote!(#codegen);
313 let expected: syn::File = parse_quote! {
314 pub type Metadata = ::std::collections::BTreeMap<::std::string::String, ::std::string::String>;
315 };
316 assert_eq!(actual, expected);
317 }
318
319 #[test]
320 fn test_container_nullable_schema() {
321 let doc = Document::from_yaml(indoc::indoc! {"
322 openapi: 3.1.0
323 info:
324 title: Test API
325 version: 1.0.0
326 paths: {}
327 components:
328 schemas:
329 NullableString:
330 type: [string, 'null']
331 NullableArray:
332 type: [array, 'null']
333 items:
334 type: string
335 NullableMap:
336 type: [object, 'null']
337 additionalProperties:
338 type: string
339 NullableOneOf:
340 oneOf:
341 - type: object
342 properties:
343 value:
344 type: string
345 - type: 'null'
346 "})
347 .unwrap();
348
349 let arena = Arena::new();
350 let spec = Spec::from_doc(&arena, &doc).unwrap();
351 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
352
353 let schema = graph.schemas().find(|s| s.name() == "NullableString");
355 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
356 panic!("expected container `NullableString`; got `{schema:?}`");
357 };
358 let codegen = CodegenSchemaType::new(schema);
359 let actual: syn::File = parse_quote!(#codegen);
360 let expected: syn::File = parse_quote! {
361 pub type NullableString = ::std::option::Option<::std::string::String>;
362 };
363 assert_eq!(actual, expected);
364
365 let schema = graph.schemas().find(|s| s.name() == "NullableArray");
367 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
368 panic!("expected container `NullableArray`; got `{schema:?}`");
369 };
370 let codegen = CodegenSchemaType::new(schema);
371 let actual: syn::File = parse_quote!(#codegen);
372 let expected: syn::File = parse_quote! {
373 pub type NullableArray = ::std::option::Option<::std::vec::Vec<::std::string::String>>;
374 };
375 assert_eq!(actual, expected);
376
377 let schema = graph.schemas().find(|s| s.name() == "NullableMap");
380 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
381 panic!("expected container `NullableMap`; got `{schema:?}`");
382 };
383 let codegen = CodegenSchemaType::new(schema);
384 let actual: syn::File = parse_quote!(#codegen);
385 let expected: syn::File = parse_quote! {
386 pub type NullableMap = ::std::option::Option<::std::collections::BTreeMap<::std::string::String, ::std::string::String>>;
387 };
388 assert_eq!(actual, expected);
389
390 let schema = graph.schemas().find(|s| s.name() == "NullableOneOf");
393 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
394 panic!("expected container `NullableOneOf`; got `{schema:?}`");
395 };
396 let codegen = CodegenSchemaType::new(schema);
397 let actual: syn::File = parse_quote!(#codegen);
398 let expected: syn::File = parse_quote! {
399 pub type NullableOneOf = ::std::option::Option<crate::types::nullable_one_of::types::V1>;
400 pub mod types {
401 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
402 #[serde(crate = "::ploidy_util::serde")]
403 #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
404 pub struct V1 {
405 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
406 pub value: ::ploidy_util::absent::AbsentOr<::std::string::String>,
407 }
408 }
409 };
410 assert_eq!(actual, expected);
411 }
412
413 #[test]
414 fn test_container_schema_preserves_description() {
415 let doc = Document::from_yaml(indoc::indoc! {"
416 openapi: 3.0.0
417 info:
418 title: Test API
419 version: 1.0.0
420 paths: {}
421 components:
422 schemas:
423 Tags:
424 description: A list of tags.
425 type: array
426 items:
427 type: string
428 "})
429 .unwrap();
430
431 let arena = Arena::new();
432 let spec = Spec::from_doc(&arena, &doc).unwrap();
433 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
434
435 let schema = graph.schemas().find(|s| s.name() == "Tags");
436 let Some(schema @ SchemaTypeView::Container(_, _)) = &schema else {
437 panic!("expected container `Tags`; got `{schema:?}`");
438 };
439
440 let codegen = CodegenSchemaType::new(schema);
441
442 let actual: syn::File = parse_quote!(#codegen);
443 let expected: syn::File = parse_quote! {
444 #[doc = "A list of tags."]
445 pub type Tags = ::std::vec::Vec<::std::string::String>;
446 };
447 assert_eq!(actual, expected);
448 }
449}