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