1use ploidy_core::{codegen::IntoCode, ir::OperationView};
2use proc_macro2::TokenStream;
3use quote::{ToTokens, TokenStreamExt, format_ident, quote};
4
5use super::{
6 cfg::CfgFeature,
7 inlines::CodegenInlines,
8 naming::{CargoFeature, CodegenIdent, CodegenIdentUsage},
9 operation::CodegenOperation,
10 query::CodegenQueryParameters,
11};
12
13pub struct CodegenResource<'a> {
16 feature: &'a CargoFeature,
17 ops: &'a [OperationView<'a>],
18}
19
20impl<'a> CodegenResource<'a> {
21 pub fn new(feature: &'a CargoFeature, ops: &'a [OperationView<'a>]) -> Self {
22 Self { feature, ops }
23 }
24}
25
26impl ToTokens for CodegenResource<'_> {
27 #[allow(
28 clippy::filter_map_bool_then,
29 reason = "`filter_map` + `then` reads cleaner here"
30 )]
31 fn to_tokens(&self, tokens: &mut TokenStream) {
32 let methods = self.ops.iter().map(|view| {
34 let cfg = CfgFeature::for_operation(view);
35 let method = CodegenOperation::new(view).into_token_stream();
36 quote! {
37 #cfg
38 #method
39 }
40 });
41 let inlines = CodegenInlines::Resource(self.ops);
42
43 let params = self
44 .ops
45 .iter()
46 .filter_map(|op| {
47 op.query().next().is_some().then(|| {
50 let cfg = CfgFeature::for_operation(op);
51 let query = CodegenQueryParameters::new(op);
52 let op_ident = CodegenIdent::new(op.id());
53 let mod_name = format_ident!("{}_query", CodegenIdentUsage::Module(&op_ident));
54 quote! {
55 #cfg
56 mod #mod_name {
57 #query
58 }
59 #cfg
60 pub use #mod_name::*;
61 }
62 })
63 })
64 .reduce(|a, b| quote!(#a #b))
65 .map(|params| {
66 quote! {
67 pub mod parameters {
68 #params
69 }
70 }
71 });
72
73 tokens.append_all(quote! {
74 impl crate::client::Client {
75 #(#methods)*
76 }
77 #params
78 #inlines
79 });
80 }
81}
82
83impl IntoCode for CodegenResource<'_> {
84 type Code = (String, TokenStream);
85
86 fn into_code(self) -> Self::Code {
87 (
88 format!(
89 "src/client/{}.rs",
90 CodegenIdentUsage::Module(self.feature.as_ident()).display()
91 ),
92 self.into_token_stream(),
93 )
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 use itertools::Itertools;
102 use ploidy_core::{
103 arena::Arena,
104 ir::{RawGraph, Spec},
105 parse::Document,
106 };
107 use pretty_assertions::assert_eq;
108 use syn::parse_quote;
109
110 use crate::{graph::CodegenGraph, naming::CargoFeature};
111
112 #[test]
115 fn test_operation_method_with_only_unnamed_deps_has_no_cfg() {
116 let doc = Document::from_yaml(indoc::indoc! {"
117 openapi: 3.0.0
118 info:
119 title: Test
120 version: 1.0.0
121 paths:
122 /customers:
123 get:
124 operationId: listCustomers
125 x-resource-name: customer
126 responses:
127 '200':
128 description: OK
129 content:
130 application/json:
131 schema:
132 type: array
133 items:
134 $ref: '#/components/schemas/Customer'
135 components:
136 schemas:
137 Customer:
138 type: object
139 properties:
140 address:
141 $ref: '#/components/schemas/Address'
142 Address:
143 type: object
144 properties:
145 street:
146 type: string
147 "})
148 .unwrap();
149
150 let arena = Arena::new();
151 let spec = Spec::from_doc(&arena, &doc).unwrap();
152 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
153
154 let ops = graph.operations().collect_vec();
155 let feature = CargoFeature::from_name("customer");
156 let resource = CodegenResource::new(&feature, &ops);
157
158 let actual: syn::File = parse_quote!(#resource);
161 let expected: syn::File = parse_quote! {
162 impl crate::client::Client {
163 pub async fn list_customers(
164 &self,
165 ) -> Result<::std::vec::Vec<crate::types::Customer>, crate::error::Error> {
166 let url = {
167 let mut url = self.base_url.clone();
168 let _ = url
169 .path_segments_mut()
170 .map(|mut segments| {
171 segments.pop_if_empty()
172 .push("customers");
173 });
174 url
175 };
176 let response = self
177 .client
178 .get(url)
179 .headers(self.headers.clone())
180 .send()
181 .await?
182 .error_for_status()?;
183 let body = response.bytes().await?;
184 let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
185 let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)
186 .map_err(crate::error::JsonError::from)?;
187 Ok(result)
188 }
189 }
190 };
191 assert_eq!(actual, expected);
192 }
193
194 #[test]
195 fn test_operation_method_with_named_deps_has_cfg() {
196 let doc = Document::from_yaml(indoc::indoc! {"
197 openapi: 3.0.0
198 info:
199 title: Test
200 version: 1.0.0
201 paths:
202 /orders:
203 get:
204 operationId: listOrders
205 x-resource-name: orders
206 responses:
207 '200':
208 description: OK
209 content:
210 application/json:
211 schema:
212 type: array
213 items:
214 $ref: '#/components/schemas/Order'
215 components:
216 schemas:
217 Order:
218 type: object
219 properties:
220 customer:
221 $ref: '#/components/schemas/Customer'
222 Customer:
223 type: object
224 x-resourceId: customer
225 properties:
226 id:
227 type: string
228 "})
229 .unwrap();
230
231 let arena = Arena::new();
232 let spec = Spec::from_doc(&arena, &doc).unwrap();
233 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
234
235 let ops = graph.operations().collect_vec();
236 let feature = CargoFeature::from_name("orders");
237 let resource = CodegenResource::new(&feature, &ops);
238
239 let actual: syn::File = parse_quote!(#resource);
242 let expected: syn::File = parse_quote! {
243 impl crate::client::Client {
244 #[cfg(feature = "customer")]
245 pub async fn list_orders(
246 &self,
247 ) -> Result<::std::vec::Vec<crate::types::Order>, crate::error::Error> {
248 let url = {
249 let mut url = self.base_url.clone();
250 let _ = url
251 .path_segments_mut()
252 .map(|mut segments| {
253 segments.pop_if_empty()
254 .push("orders");
255 });
256 url
257 };
258 let response = self
259 .client
260 .get(url)
261 .headers(self.headers.clone())
262 .send()
263 .await?
264 .error_for_status()?;
265 let body = response.bytes().await?;
266 let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
267 let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)
268 .map_err(crate::error::JsonError::from)?;
269 Ok(result)
270 }
271 }
272 };
273 assert_eq!(actual, expected);
274 }
275
276 #[test]
279 fn test_resource_emits_parameters_module() {
280 let doc = Document::from_yaml(indoc::indoc! {"
281 openapi: 3.0.0
282 info:
283 title: Test
284 version: 1.0.0
285 paths:
286 /customers:
287 get:
288 operationId: listCustomers
289 x-resource-name: customer
290 parameters:
291 - name: limit
292 in: query
293 schema:
294 type: integer
295 format: int32
296 responses:
297 '200':
298 description: OK
299 "})
300 .unwrap();
301
302 let arena = Arena::new();
303 let spec = Spec::from_doc(&arena, &doc).unwrap();
304 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
305
306 let ops = graph.operations().collect_vec();
307 let feature = CargoFeature::from_name("customer");
308 let resource = CodegenResource::new(&feature, &ops);
309
310 let actual: syn::File = parse_quote!(#resource);
311 let expected: syn::File = parse_quote! {
312 impl crate::client::Client {
313 pub async fn list_customers(
314 &self,
315 query: ¶meters::ListCustomersQuery
316 ) -> Result<(), crate::error::Error> {
317 let url = {
318 let mut url = self.base_url.clone();
319 let _ = url
320 .path_segments_mut()
321 .map(|mut segments| {
322 segments.pop_if_empty()
323 .push("customers");
324 });
325 url
326 };
327 let url = ::ploidy_util::serde::Serialize::serialize(
328 query,
329 ::ploidy_util::QuerySerializer::new(
330 url,
331 parameters::ListCustomersQuery::STYLES,
332 ),
333 )?;
334 let response = self
335 .client
336 .get(url)
337 .headers(self.headers.clone())
338 .send()
339 .await?
340 .error_for_status()?;
341 let _ = response;
342 Ok(())
343 }
344 }
345 pub mod parameters {
346 mod list_customers_query {
347 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
348 #[serde(crate = "::ploidy_util::serde")]
349 pub struct ListCustomersQuery {
350 #[serde(default, skip_serializing_if = "Option::is_none")]
351 pub limit: ::std::option::Option<i32>,
352 }
353 impl ListCustomersQuery {
354 pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[];
355 }
356 }
357 pub use list_customers_query::*;
358 }
359 };
360 assert_eq!(actual, expected);
361 }
362
363 #[test]
364 fn test_resource_with_multiple_query_ops_shares_parameters_module() {
365 let doc = Document::from_yaml(indoc::indoc! {"
366 openapi: 3.0.0
367 info:
368 title: Test
369 version: 1.0.0
370 paths:
371 /customers:
372 get:
373 operationId: listCustomers
374 x-resource-name: customer
375 parameters:
376 - name: limit
377 in: query
378 schema:
379 type: integer
380 format: int32
381 responses:
382 '200':
383 description: OK
384 /customers/search:
385 get:
386 operationId: searchCustomers
387 x-resource-name: customer
388 parameters:
389 - name: email
390 in: query
391 required: true
392 schema:
393 type: string
394 responses:
395 '200':
396 description: OK
397 "})
398 .unwrap();
399
400 let arena = Arena::new();
401 let spec = Spec::from_doc(&arena, &doc).unwrap();
402 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
403
404 let ops = graph.operations().collect_vec();
405 let feature = CargoFeature::from_name("customer");
406 let resource = CodegenResource::new(&feature, &ops);
407
408 let actual: syn::File = parse_quote!(#resource);
409 let expected: syn::File = parse_quote! {
410 impl crate::client::Client {
411 pub async fn list_customers(
412 &self,
413 query: ¶meters::ListCustomersQuery
414 ) -> Result<(), crate::error::Error> {
415 let url = {
416 let mut url = self.base_url.clone();
417 let _ = url
418 .path_segments_mut()
419 .map(|mut segments| {
420 segments.pop_if_empty()
421 .push("customers");
422 });
423 url
424 };
425 let url = ::ploidy_util::serde::Serialize::serialize(
426 query,
427 ::ploidy_util::QuerySerializer::new(
428 url,
429 parameters::ListCustomersQuery::STYLES,
430 ),
431 )?;
432 let response = self
433 .client
434 .get(url)
435 .headers(self.headers.clone())
436 .send()
437 .await?
438 .error_for_status()?;
439 let _ = response;
440 Ok(())
441 }
442 pub async fn search_customers(
443 &self,
444 query: ¶meters::SearchCustomersQuery
445 ) -> Result<(), crate::error::Error> {
446 let url = {
447 let mut url = self.base_url.clone();
448 let _ = url
449 .path_segments_mut()
450 .map(|mut segments| {
451 segments.pop_if_empty()
452 .push("customers")
453 .push("search");
454 });
455 url
456 };
457 let url = ::ploidy_util::serde::Serialize::serialize(
458 query,
459 ::ploidy_util::QuerySerializer::new(
460 url,
461 parameters::SearchCustomersQuery::STYLES,
462 ),
463 )?;
464 let response = self
465 .client
466 .get(url)
467 .headers(self.headers.clone())
468 .send()
469 .await?
470 .error_for_status()?;
471 let _ = response;
472 Ok(())
473 }
474 }
475 pub mod parameters {
476 mod list_customers_query {
477 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
478 #[serde(crate = "::ploidy_util::serde")]
479 pub struct ListCustomersQuery {
480 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub limit: ::std::option::Option<i32>,
482 }
483 impl ListCustomersQuery {
484 pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[];
485 }
486 }
487 pub use list_customers_query::*;
488 mod search_customers_query {
489 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
490 #[serde(crate = "::ploidy_util::serde")]
491 pub struct SearchCustomersQuery {
492 pub email: ::std::string::String,
493 }
494 impl SearchCustomersQuery {
495 pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[];
496 }
497 }
498 pub use search_customers_query::*;
499 }
500 };
501 assert_eq!(actual, expected);
502 }
503
504 #[test]
505 fn test_resource_omits_parameters_module_when_no_query_params() {
506 let doc = Document::from_yaml(indoc::indoc! {"
507 openapi: 3.0.0
508 info:
509 title: Test
510 version: 1.0.0
511 paths:
512 /customers:
513 get:
514 operationId: listCustomers
515 x-resource-name: customer
516 responses:
517 '200':
518 description: OK
519 "})
520 .unwrap();
521
522 let arena = Arena::new();
523 let spec = Spec::from_doc(&arena, &doc).unwrap();
524 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
525
526 let ops = graph.operations().collect_vec();
527 let feature = CargoFeature::from_name("customer");
528 let resource = CodegenResource::new(&feature, &ops);
529
530 let actual: syn::File = parse_quote!(#resource);
531 let expected: syn::File = parse_quote! {
532 impl crate::client::Client {
533 pub async fn list_customers(
534 &self,
535 ) -> Result<(), crate::error::Error> {
536 let url = {
537 let mut url = self.base_url.clone();
538 let _ = url
539 .path_segments_mut()
540 .map(|mut segments| {
541 segments.pop_if_empty()
542 .push("customers");
543 });
544 url
545 };
546 let response = self
547 .client
548 .get(url)
549 .headers(self.headers.clone())
550 .send()
551 .await?
552 .error_for_status()?;
553 let _ = response;
554 Ok(())
555 }
556 }
557 };
558 assert_eq!(actual, expected);
559 }
560}