1use itertools::Itertools;
2use ploidy_core::ir::{InlineTypeView, OperationView, SchemaTypeView, View};
3use proc_macro2::TokenStream;
4use quote::{ToTokens, TokenStreamExt, quote};
5
6use super::{
7 cfg::CfgFeature,
8 enum_::CodegenEnum,
9 naming::{CodegenTypeName, CodegenTypeNameSortKey},
10 struct_::CodegenStruct,
11 tagged::CodegenTagged,
12 untagged::CodegenUntagged,
13};
14
15#[derive(Clone, Copy, Debug)]
21pub enum CodegenInlines<'a> {
22 Resource(&'a [OperationView<'a>]),
23 Schema(&'a SchemaTypeView<'a>),
24}
25
26impl ToTokens for CodegenInlines<'_> {
27 fn to_tokens(&self, tokens: &mut TokenStream) {
28 match self {
29 Self::Resource(ops) => {
30 let items = CodegenInlineItems(IncludeCfgFeatures::Include, ops);
31 items.to_tokens(tokens);
32 }
33 &Self::Schema(ty) => {
34 let items = CodegenInlineItems(IncludeCfgFeatures::Omit, std::slice::from_ref(ty));
35 items.to_tokens(tokens);
36 }
37 }
38 }
39}
40
41#[derive(Debug)]
42struct CodegenInlineItems<'a, V>(IncludeCfgFeatures, &'a [V]);
43
44impl<'a, V> ToTokens for CodegenInlineItems<'a, V>
45where
46 V: View<'a>,
47{
48 fn to_tokens(&self, tokens: &mut TokenStream) {
49 let mut inlines = self.1.iter().flat_map(|op| op.inlines()).collect_vec();
50 inlines.sort_by(|a, b| {
51 CodegenTypeNameSortKey::for_inline(a).cmp(&CodegenTypeNameSortKey::for_inline(b))
52 });
53
54 let mut items = inlines.into_iter().filter_map(|view| {
55 let name = CodegenTypeName::Inline(&view);
56 let ty = match &view {
57 InlineTypeView::Enum(_, view) => CodegenEnum::new(name, view).into_token_stream(),
58 InlineTypeView::Struct(_, view) => {
59 CodegenStruct::new(name, view).into_token_stream()
60 }
61 InlineTypeView::Tagged(_, view) => {
62 CodegenTagged::new(name, view).into_token_stream()
63 }
64 InlineTypeView::Untagged(_, view) => {
65 CodegenUntagged::new(name, view).into_token_stream()
66 }
67 InlineTypeView::Container(..)
68 | InlineTypeView::Primitive(..)
69 | InlineTypeView::Any(..) => {
70 return None;
73 }
74 };
75 Some(match self.0 {
76 IncludeCfgFeatures::Include => {
77 let cfg = CfgFeature::for_inline_type(&view);
80 let mod_name = name.into_module_name();
81 quote! {
82 #cfg
83 mod #mod_name {
84 #ty
85 }
86 #cfg
87 pub use #mod_name::*;
88 }
89 }
90 IncludeCfgFeatures::Omit => ty,
91 })
92 });
93
94 if let Some(first) = items.next() {
95 tokens.append_all(quote! {
96 pub mod types {
97 #first
98 #(#items)*
99 }
100 });
101 }
102 }
103}
104
105#[derive(Clone, Copy, Debug)]
106enum IncludeCfgFeatures {
107 Include,
108 Omit,
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 use itertools::Itertools;
116 use ploidy_core::{
117 arena::Arena,
118 ir::{RawGraph, Spec},
119 parse::Document,
120 };
121 use pretty_assertions::assert_eq;
122 use syn::parse_quote;
123
124 use crate::graph::CodegenGraph;
125
126 #[test]
127 fn test_includes_inline_types_from_operation_parameters() {
128 let doc = Document::from_yaml(indoc::indoc! {"
129 openapi: 3.0.0
130 info:
131 title: Test API
132 version: 1.0.0
133 paths:
134 /items:
135 get:
136 operationId: getItems
137 parameters:
138 - name: filter
139 in: query
140 schema:
141 type: object
142 properties:
143 status:
144 type: string
145 responses:
146 '200':
147 description: OK
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 ops = graph.operations().collect_vec();
156 let inlines = CodegenInlines::Resource(&ops);
157
158 let actual: syn::File = parse_quote!(#inlines);
159 let expected: syn::File = parse_quote! {
160 pub mod types {
161 mod get_items_filter {
162 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
163 #[serde(crate = "::ploidy_util::serde")]
164 pub struct GetItemsFilter {
165 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
166 pub status: ::ploidy_util::absent::AbsentOr<::std::string::String>,
167 }
168 }
169 pub use get_items_filter::*;
170 }
171 };
172 assert_eq!(actual, expected);
173 }
174
175 #[test]
176 fn test_excludes_inline_types_from_referenced_schemas() {
177 let doc = Document::from_yaml(indoc::indoc! {"
181 openapi: 3.0.0
182 info:
183 title: Test API
184 version: 1.0.0
185 paths:
186 /items:
187 get:
188 operationId: getItems
189 responses:
190 '200':
191 description: OK
192 content:
193 application/json:
194 schema:
195 $ref: '#/components/schemas/Item'
196 components:
197 schemas:
198 Item:
199 type: object
200 properties:
201 details:
202 type: object
203 properties:
204 description:
205 type: string
206 "})
207 .unwrap();
208
209 let arena = Arena::new();
210 let spec = Spec::from_doc(&arena, &doc).unwrap();
211 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
212
213 let ops = graph.operations().collect_vec();
214 let inlines = CodegenInlines::Resource(&ops);
215
216 let actual: syn::File = parse_quote!(#inlines);
219 let expected: syn::File = parse_quote! {};
220 assert_eq!(actual, expected);
221 }
222
223 #[test]
224 fn test_sorts_inline_types_alphabetically() {
225 let doc = Document::from_yaml(indoc::indoc! {"
227 openapi: 3.0.0
228 info:
229 title: Test API
230 version: 1.0.0
231 paths:
232 /items:
233 get:
234 operationId: getItems
235 parameters:
236 - name: zebra
237 in: query
238 schema:
239 type: object
240 properties:
241 value:
242 type: string
243 - name: mango
244 in: query
245 schema:
246 type: object
247 properties:
248 value:
249 type: string
250 - name: apple
251 in: query
252 schema:
253 type: object
254 properties:
255 value:
256 type: string
257 responses:
258 '200':
259 description: OK
260 "})
261 .unwrap();
262
263 let arena = Arena::new();
264 let spec = Spec::from_doc(&arena, &doc).unwrap();
265 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
266
267 let ops = graph.operations().collect_vec();
268 let inlines = CodegenInlines::Resource(&ops);
269
270 let actual: syn::File = parse_quote!(#inlines);
271 let expected: syn::File = parse_quote! {
273 pub mod types {
274 mod get_items_apple {
275 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
276 #[serde(crate = "::ploidy_util::serde")]
277 pub struct GetItemsApple {
278 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
279 pub value: ::ploidy_util::absent::AbsentOr<::std::string::String>,
280 }
281 }
282 pub use get_items_apple::*;
283 mod get_items_mango {
284 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
285 #[serde(crate = "::ploidy_util::serde")]
286 pub struct GetItemsMango {
287 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
288 pub value: ::ploidy_util::absent::AbsentOr<::std::string::String>,
289 }
290 }
291 pub use get_items_mango::*;
292 mod get_items_zebra {
293 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
294 #[serde(crate = "::ploidy_util::serde")]
295 pub struct GetItemsZebra {
296 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
297 pub value: ::ploidy_util::absent::AbsentOr<::std::string::String>,
298 }
299 }
300 pub use get_items_zebra::*;
301 }
302 };
303 assert_eq!(actual, expected);
304 }
305
306 #[test]
307 fn test_no_output_when_no_inline_types() {
308 let doc = Document::from_yaml(indoc::indoc! {"
309 openapi: 3.0.0
310 info:
311 title: Test API
312 version: 1.0.0
313 paths:
314 /items:
315 get:
316 operationId: getItems
317 parameters:
318 - name: limit
319 in: query
320 schema:
321 type: integer
322 responses:
323 '200':
324 description: OK
325 "})
326 .unwrap();
327
328 let arena = Arena::new();
329 let spec = Spec::from_doc(&arena, &doc).unwrap();
330 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
331
332 let ops = graph.operations().collect_vec();
333 let inlines = CodegenInlines::Resource(&ops);
334
335 let actual: syn::File = parse_quote!(#inlines);
336 let expected: syn::File = parse_quote! {};
337 assert_eq!(actual, expected);
338 }
339
340 #[test]
341 fn test_finds_inline_types_within_optionals() {
342 let doc = Document::from_yaml(indoc::indoc! {"
343 openapi: 3.0.0
344 info:
345 title: Test API
346 version: 1.0.0
347 paths:
348 /items:
349 get:
350 operationId: getItems
351 parameters:
352 - name: config
353 in: query
354 schema:
355 nullable: true
356 type: object
357 properties:
358 enabled:
359 type: boolean
360 responses:
361 '200':
362 description: OK
363 "})
364 .unwrap();
365
366 let arena = Arena::new();
367 let spec = Spec::from_doc(&arena, &doc).unwrap();
368 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
369
370 let ops = graph.operations().collect_vec();
371 let inlines = CodegenInlines::Resource(&ops);
372
373 let actual: syn::File = parse_quote!(#inlines);
374 let expected: syn::File = parse_quote! {
375 pub mod types {
376 mod get_items_config {
377 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
378 #[serde(crate = "::ploidy_util::serde")]
379 pub struct GetItemsConfig {
380 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
381 pub enabled: ::ploidy_util::absent::AbsentOr<bool>,
382 }
383 }
384 pub use get_items_config::*;
385 }
386 };
387 assert_eq!(actual, expected);
388 }
389
390 #[test]
391 fn test_finds_inline_types_within_arrays() {
392 let doc = Document::from_yaml(indoc::indoc! {"
393 openapi: 3.0.0
394 info:
395 title: Test API
396 version: 1.0.0
397 paths:
398 /items:
399 get:
400 operationId: getItems
401 parameters:
402 - name: filters
403 in: query
404 schema:
405 type: array
406 items:
407 type: object
408 properties:
409 field:
410 type: string
411 responses:
412 '200':
413 description: OK
414 "})
415 .unwrap();
416
417 let arena = Arena::new();
418 let spec = Spec::from_doc(&arena, &doc).unwrap();
419 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
420
421 let ops = graph.operations().collect_vec();
422 let inlines = CodegenInlines::Resource(&ops);
423
424 let actual: syn::File = parse_quote!(#inlines);
425 let expected: syn::File = parse_quote! {
426 pub mod types {
427 mod get_items_filters_item {
428 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
429 #[serde(crate = "::ploidy_util::serde")]
430 pub struct GetItemsFiltersItem {
431 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
432 pub field: ::ploidy_util::absent::AbsentOr<::std::string::String>,
433 }
434 }
435 pub use get_items_filters_item::*;
436 }
437 };
438 assert_eq!(actual, expected);
439 }
440
441 #[test]
442 fn test_finds_inline_types_within_maps() {
443 let doc = Document::from_yaml(indoc::indoc! {"
444 openapi: 3.0.0
445 info:
446 title: Test API
447 version: 1.0.0
448 paths:
449 /items:
450 get:
451 operationId: getItems
452 parameters:
453 - name: metadata
454 in: query
455 schema:
456 type: object
457 additionalProperties:
458 type: object
459 properties:
460 value:
461 type: string
462 responses:
463 '200':
464 description: OK
465 "})
466 .unwrap();
467
468 let arena = Arena::new();
469 let spec = Spec::from_doc(&arena, &doc).unwrap();
470 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
471
472 let ops = graph.operations().collect_vec();
473 let inlines = CodegenInlines::Resource(&ops);
474
475 let actual: syn::File = parse_quote!(#inlines);
476 let expected: syn::File = parse_quote! {
477 pub mod types {
478 mod get_items_metadata_value {
479 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
480 #[serde(crate = "::ploidy_util::serde")]
481 pub struct GetItemsMetadataValue {
482 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
483 pub value: ::ploidy_util::absent::AbsentOr<::std::string::String>,
484 }
485 }
486 pub use get_items_metadata_value::*;
487 }
488 };
489 assert_eq!(actual, expected);
490 }
491}