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