Skip to main content

ploidy_codegen_rust/
resource.rs

1use ploidy_core::{codegen::IntoCode, ir::IrOperationView};
2use proc_macro2::TokenStream;
3use quote::{ToTokens, TokenStreamExt, quote};
4
5use super::{
6    cfg::CfgFeature,
7    inlines::CodegenInlines,
8    naming::{CargoFeature, CodegenIdentUsage},
9    operation::CodegenOperation,
10};
11
12/// Generates an `impl Client` block for a feature-gated resource,
13/// with all its operations and inline types.
14pub struct CodegenResource<'a> {
15    feature: &'a CargoFeature,
16    ops: &'a [IrOperationView<'a>],
17}
18
19impl<'a> CodegenResource<'a> {
20    pub fn new(feature: &'a CargoFeature, ops: &'a [IrOperationView<'a>]) -> Self {
21        Self { feature, ops }
22    }
23}
24
25impl ToTokens for CodegenResource<'_> {
26    fn to_tokens(&self, tokens: &mut TokenStream) {
27        // Each method gets its own `#[cfg(...)]` attribute.
28        let methods = self.ops.iter().map(|view| {
29            let cfg = CfgFeature::for_operation(view);
30            let method = CodegenOperation::new(view).into_token_stream();
31            quote! {
32                #cfg
33                #method
34            }
35        });
36        let inlines = CodegenInlines::Resource(self.ops);
37        tokens.append_all(quote! {
38            impl crate::client::Client {
39                #(#methods)*
40            }
41            #inlines
42        });
43    }
44}
45
46impl IntoCode for CodegenResource<'_> {
47    type Code = (String, TokenStream);
48
49    fn into_code(self) -> Self::Code {
50        (
51            format!(
52                "src/client/{}.rs",
53                CodegenIdentUsage::Module(self.feature.as_ident()).display()
54            ),
55            self.into_token_stream(),
56        )
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    use itertools::Itertools;
65    use ploidy_core::{
66        ir::{IrGraph, IrSpec},
67        parse::Document,
68    };
69    use pretty_assertions::assert_eq;
70
71    use syn::parse_quote;
72
73    use crate::{graph::CodegenGraph, naming::CargoFeature};
74
75    #[test]
76    fn test_operation_method_with_only_unnamed_deps_has_no_cfg() {
77        let doc = Document::from_yaml(indoc::indoc! {"
78            openapi: 3.0.0
79            info:
80              title: Test
81              version: 1.0.0
82            paths:
83              /customers:
84                get:
85                  operationId: listCustomers
86                  x-resource-name: customer
87                  responses:
88                    '200':
89                      description: OK
90                      content:
91                        application/json:
92                          schema:
93                            type: array
94                            items:
95                              $ref: '#/components/schemas/Customer'
96            components:
97              schemas:
98                Customer:
99                  type: object
100                  properties:
101                    address:
102                      $ref: '#/components/schemas/Address'
103                Address:
104                  type: object
105                  properties:
106                    street:
107                      type: string
108        "})
109        .unwrap();
110
111        let spec = IrSpec::from_doc(&doc).unwrap();
112        let ir = IrGraph::new(&spec);
113        let graph = CodegenGraph::new(ir);
114
115        let ops = graph.operations().collect_vec();
116        let feature = CargoFeature::from_name("customer");
117        let resource = CodegenResource::new(&feature, &ops);
118
119        // Parse the generated tokens as a file, then
120        // extract the `impl` block containing the methods.
121        let actual: syn::File = parse_quote!(#resource);
122        let block = actual
123            .items
124            .iter()
125            .find_map(|item| match item {
126                syn::Item::Impl(block) => Some(block),
127                _ => None,
128            })
129            .unwrap();
130
131        // The method should not have a `#[cfg(...)]` attribute,
132        // since none of its dependencies have an `x-resourceId`.
133        let methods = block
134            .items
135            .iter()
136            .filter_map(|item| match item {
137                syn::ImplItem::Fn(method) => Some(method),
138                _ => None,
139            })
140            .collect_vec();
141        assert_eq!(methods.len(), 1);
142        assert!(
143            !methods[0]
144                .attrs
145                .iter()
146                .any(|attr| attr.path().is_ident("cfg"))
147        );
148    }
149
150    #[test]
151    fn test_operation_method_with_named_deps_has_cfg() {
152        let doc = Document::from_yaml(indoc::indoc! {"
153            openapi: 3.0.0
154            info:
155              title: Test
156              version: 1.0.0
157            paths:
158              /orders:
159                get:
160                  operationId: listOrders
161                  x-resource-name: orders
162                  responses:
163                    '200':
164                      description: OK
165                      content:
166                        application/json:
167                          schema:
168                            type: array
169                            items:
170                              $ref: '#/components/schemas/Order'
171            components:
172              schemas:
173                Order:
174                  type: object
175                  properties:
176                    customer:
177                      $ref: '#/components/schemas/Customer'
178                Customer:
179                  type: object
180                  x-resourceId: customer
181                  properties:
182                    id:
183                      type: string
184        "})
185        .unwrap();
186
187        let spec = IrSpec::from_doc(&doc).unwrap();
188        let ir = IrGraph::new(&spec);
189        let graph = CodegenGraph::new(ir);
190
191        let ops = graph.operations().collect_vec();
192        let feature = CargoFeature::from_name("orders");
193        let resource = CodegenResource::new(&feature, &ops);
194
195        let actual: syn::File = parse_quote!(#resource);
196        let block = actual
197            .items
198            .iter()
199            .find_map(|item| match item {
200                syn::Item::Impl(block) => Some(block),
201                _ => None,
202            })
203            .unwrap();
204
205        // The method should have a `#[cfg(feature = "customer")]` attribute,
206        // since `Order` (no resource) depends on `Customer` (has `x-resourceId`).
207        let methods = block
208            .items
209            .iter()
210            .filter_map(|item| match item {
211                syn::ImplItem::Fn(method) => Some(method),
212                _ => None,
213            })
214            .collect_vec();
215        assert_eq!(methods.len(), 1);
216        let cfg = methods[0]
217            .attrs
218            .iter()
219            .find(|attr| attr.path().is_ident("cfg"));
220        let expected: syn::Attribute = parse_quote!(#[cfg(feature = "customer")]);
221        assert_eq!(cfg, Some(&expected));
222    }
223}