Skip to main content

ploidy_codegen_rust/
inlines.rs

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/// Generates an inline `mod types`, with definitions for all the inline types
16/// that are reachable from a resource or schema type.
17///
18/// Inline types nested _within_ referenced schemas are excluded; those are
19/// emitted by [`CodegenSchemaType`](crate::CodegenSchemaType) instead.
20#[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                    // Wrap each type in an inner inline module, so that
71                    // the `#[cfg(...)]` applies to all items (types and `impl`s).
72                    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        // The operation references `Item`, which has an inline type `Details`.
170        // `Details` should _not_ be emitted by `CodegenInlines`; it belongs in
171        // the schema's module instead.
172        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        // No inline types should be emitted, since the only inline (`Details`)
209        // belongs to the referenced schema.
210        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        // Parameters defined in reverse order: zebra, mango, apple.
218        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        // Types should be sorted: Apple, Mango, Zebra.
264        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}