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, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
163 #[serde(crate = "::ploidy_util::serde")]
164 #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
165 pub struct GetItemsFilter {
166 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
167 pub status: ::ploidy_util::absent::AbsentOr<::std::string::String>,
168 }
169 }
170 pub use get_items_filter::*;
171 }
172 };
173 assert_eq!(actual, expected);
174 }
175
176 #[test]
177 fn test_excludes_inline_types_from_referenced_schemas() {
178 let doc = Document::from_yaml(indoc::indoc! {"
182 openapi: 3.0.0
183 info:
184 title: Test API
185 version: 1.0.0
186 paths:
187 /items:
188 get:
189 operationId: getItems
190 responses:
191 '200':
192 description: OK
193 content:
194 application/json:
195 schema:
196 $ref: '#/components/schemas/Item'
197 components:
198 schemas:
199 Item:
200 type: object
201 properties:
202 details:
203 type: object
204 properties:
205 description:
206 type: string
207 "})
208 .unwrap();
209
210 let arena = Arena::new();
211 let spec = Spec::from_doc(&arena, &doc).unwrap();
212 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
213
214 let ops = graph.operations().collect_vec();
215 let inlines = CodegenInlines::Resource(&ops);
216
217 let actual: syn::File = parse_quote!(#inlines);
220 let expected: syn::File = parse_quote! {};
221 assert_eq!(actual, expected);
222 }
223
224 #[test]
225 fn test_sorts_inline_types_alphabetically() {
226 let doc = Document::from_yaml(indoc::indoc! {"
228 openapi: 3.0.0
229 info:
230 title: Test API
231 version: 1.0.0
232 paths:
233 /items:
234 get:
235 operationId: getItems
236 parameters:
237 - name: zebra
238 in: query
239 schema:
240 type: object
241 properties:
242 value:
243 type: string
244 - name: mango
245 in: query
246 schema:
247 type: object
248 properties:
249 value:
250 type: string
251 - name: apple
252 in: query
253 schema:
254 type: object
255 properties:
256 value:
257 type: string
258 responses:
259 '200':
260 description: OK
261 "})
262 .unwrap();
263
264 let arena = Arena::new();
265 let spec = Spec::from_doc(&arena, &doc).unwrap();
266 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
267
268 let ops = graph.operations().collect_vec();
269 let inlines = CodegenInlines::Resource(&ops);
270
271 let actual: syn::File = parse_quote!(#inlines);
272 let expected: syn::File = parse_quote! {
274 pub mod types {
275 mod get_items_apple {
276 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
277 #[serde(crate = "::ploidy_util::serde")]
278 #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
279 pub struct GetItemsApple {
280 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
281 pub value: ::ploidy_util::absent::AbsentOr<::std::string::String>,
282 }
283 }
284 pub use get_items_apple::*;
285 mod get_items_mango {
286 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
287 #[serde(crate = "::ploidy_util::serde")]
288 #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
289 pub struct GetItemsMango {
290 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
291 pub value: ::ploidy_util::absent::AbsentOr<::std::string::String>,
292 }
293 }
294 pub use get_items_mango::*;
295 mod get_items_zebra {
296 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
297 #[serde(crate = "::ploidy_util::serde")]
298 #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
299 pub struct GetItemsZebra {
300 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
301 pub value: ::ploidy_util::absent::AbsentOr<::std::string::String>,
302 }
303 }
304 pub use get_items_zebra::*;
305 }
306 };
307 assert_eq!(actual, expected);
308 }
309
310 #[test]
311 fn test_no_output_when_no_inline_types() {
312 let doc = Document::from_yaml(indoc::indoc! {"
313 openapi: 3.0.0
314 info:
315 title: Test API
316 version: 1.0.0
317 paths:
318 /items:
319 get:
320 operationId: getItems
321 parameters:
322 - name: limit
323 in: query
324 schema:
325 type: integer
326 responses:
327 '200':
328 description: OK
329 "})
330 .unwrap();
331
332 let arena = Arena::new();
333 let spec = Spec::from_doc(&arena, &doc).unwrap();
334 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
335
336 let ops = graph.operations().collect_vec();
337 let inlines = CodegenInlines::Resource(&ops);
338
339 let actual: syn::File = parse_quote!(#inlines);
340 let expected: syn::File = parse_quote! {};
341 assert_eq!(actual, expected);
342 }
343
344 #[test]
345 fn test_finds_inline_types_within_optionals() {
346 let doc = Document::from_yaml(indoc::indoc! {"
347 openapi: 3.0.0
348 info:
349 title: Test API
350 version: 1.0.0
351 paths:
352 /items:
353 get:
354 operationId: getItems
355 parameters:
356 - name: config
357 in: query
358 schema:
359 nullable: true
360 type: object
361 properties:
362 enabled:
363 type: boolean
364 responses:
365 '200':
366 description: OK
367 "})
368 .unwrap();
369
370 let arena = Arena::new();
371 let spec = Spec::from_doc(&arena, &doc).unwrap();
372 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
373
374 let ops = graph.operations().collect_vec();
375 let inlines = CodegenInlines::Resource(&ops);
376
377 let actual: syn::File = parse_quote!(#inlines);
378 let expected: syn::File = parse_quote! {
379 pub mod types {
380 mod get_items_config {
381 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
382 #[serde(crate = "::ploidy_util::serde")]
383 #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
384 pub struct GetItemsConfig {
385 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
386 pub enabled: ::ploidy_util::absent::AbsentOr<bool>,
387 }
388 }
389 pub use get_items_config::*;
390 }
391 };
392 assert_eq!(actual, expected);
393 }
394
395 #[test]
396 fn test_finds_inline_types_within_arrays() {
397 let doc = Document::from_yaml(indoc::indoc! {"
398 openapi: 3.0.0
399 info:
400 title: Test API
401 version: 1.0.0
402 paths:
403 /items:
404 get:
405 operationId: getItems
406 parameters:
407 - name: filters
408 in: query
409 schema:
410 type: array
411 items:
412 type: object
413 properties:
414 field:
415 type: string
416 responses:
417 '200':
418 description: OK
419 "})
420 .unwrap();
421
422 let arena = Arena::new();
423 let spec = Spec::from_doc(&arena, &doc).unwrap();
424 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
425
426 let ops = graph.operations().collect_vec();
427 let inlines = CodegenInlines::Resource(&ops);
428
429 let actual: syn::File = parse_quote!(#inlines);
430 let expected: syn::File = parse_quote! {
431 pub mod types {
432 mod get_items_filters_item {
433 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
434 #[serde(crate = "::ploidy_util::serde")]
435 #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
436 pub struct GetItemsFiltersItem {
437 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
438 pub field: ::ploidy_util::absent::AbsentOr<::std::string::String>,
439 }
440 }
441 pub use get_items_filters_item::*;
442 }
443 };
444 assert_eq!(actual, expected);
445 }
446
447 #[test]
448 fn test_finds_inline_types_within_maps() {
449 let doc = Document::from_yaml(indoc::indoc! {"
450 openapi: 3.0.0
451 info:
452 title: Test API
453 version: 1.0.0
454 paths:
455 /items:
456 get:
457 operationId: getItems
458 parameters:
459 - name: metadata
460 in: query
461 schema:
462 type: object
463 additionalProperties:
464 type: object
465 properties:
466 value:
467 type: string
468 responses:
469 '200':
470 description: OK
471 "})
472 .unwrap();
473
474 let arena = Arena::new();
475 let spec = Spec::from_doc(&arena, &doc).unwrap();
476 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
477
478 let ops = graph.operations().collect_vec();
479 let inlines = CodegenInlines::Resource(&ops);
480
481 let actual: syn::File = parse_quote!(#inlines);
482 let expected: syn::File = parse_quote! {
483 pub mod types {
484 mod get_items_metadata_value {
485 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize, ::ploidy_util::pointer::JsonPointee, ::ploidy_util::pointer::JsonPointerTarget)]
486 #[serde(crate = "::ploidy_util::serde")]
487 #[ploidy(pointer(crate = "::ploidy_util::pointer"))]
488 pub struct GetItemsMetadataValue {
489 #[serde(default, skip_serializing_if = "::ploidy_util::absent::AbsentOr::is_absent")]
490 pub value: ::ploidy_util::absent::AbsentOr<::std::string::String>,
491 }
492 }
493 pub use get_items_metadata_value::*;
494 }
495 };
496 assert_eq!(actual, expected);
497 }
498}