Skip to main content

ploidy_codegen_rust/
inlines.rs

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/// 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 [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                    // Container types, primitive types, and untyped values
71                    // are emitted directly; they don't need type aliases.
72                    return None;
73                }
74            };
75            Some(match self.0 {
76                IncludeCfgFeatures::Include => {
77                    // Wrap each type in an inner inline module, so that
78                    // the `#[cfg(...)]` applies to all items (types and `impl`s).
79                    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        // The operation references `Item`, which has an inline type `Details`.
178        // `Details` should _not_ be emitted by `CodegenInlines`; it belongs in
179        // the schema's module instead.
180        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        // No inline types should be emitted, since the only inline (`Details`)
217        // belongs to the referenced schema.
218        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        // Parameters defined in reverse order: zebra, mango, apple.
226        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        // Types should be sorted: Apple, Mango, Zebra.
272        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}