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 .extend(&["customers", "search"]);
493 });
494 url
495 };
496 let url = ::ploidy_util::serde::Serialize::serialize(
497 query,
498 ::ploidy_util::QuerySerializer::new(
499 url,
500 parameters::SearchCustomersQuery::STYLES,
501 ),
502 )?;
503 let response = self
504 .client
505 .get(url)
506 .headers(self.headers.clone())
507 .send()
508 .await?
509 .error_for_status()?;
510 let _ = response;
511 Ok(())
512 }
513 }
514 pub mod parameters {
515 mod list_customers_query {
516 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
517 #[serde(crate = "::ploidy_util::serde")]
518 pub struct ListCustomersQuery {
519 #[serde(default, skip_serializing_if = "Option::is_none")]
520 pub limit: ::std::option::Option<i32>,
521 }
522 impl ListCustomersQuery {
523 pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[];
524 }
525 }
526 pub use list_customers_query::*;
527 mod search_customers_query {
528 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
529 #[serde(crate = "::ploidy_util::serde")]
530 pub struct SearchCustomersQuery {
531 pub email: ::std::string::String,
532 }
533 impl SearchCustomersQuery {
534 pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[];
535 }
536 }
537 pub use search_customers_query::*;
538 }
539 };
540 assert_eq!(actual, expected);
541 }
542
543 #[test]
544 fn test_resource_omits_parameters_module_when_no_query_params() {
545 let doc = Document::from_yaml(indoc::indoc! {"
546 openapi: 3.0.0
547 info:
548 title: Test
549 version: 1.0.0
550 paths:
551 /customers:
552 get:
553 operationId: listCustomers
554 x-resource-name: customer
555 responses:
556 '200':
557 description: OK
558 "})
559 .unwrap();
560
561 let arena = Arena::new();
562 let spec = Spec::from_doc(&arena, &doc).unwrap();
563 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
564
565 let ops = graph.operations().collect_vec();
566 let [op] = &*ops else {
567 panic!("expected one operation; got `{ops:?}`");
568 };
569 let resource =
570 CodegenResource::new(&graph, graph.resource_for(op), std::slice::from_ref(op));
571
572 let actual: syn::File = parse_quote!(#resource);
573 let expected: syn::File = parse_quote! {
574 impl crate::client::Client {
575 #[doc = " GET /customers"]
576 pub async fn list_customers(
577 &self,
578 ) -> Result<(), crate::error::Error> {
579 let url = {
580 let mut url = self.base_url.clone();
581 let _ = url
582 .path_segments_mut()
583 .map(|mut segments| {
584 segments.pop_if_empty()
585 .push("customers");
586 });
587 url
588 };
589 let response = self
590 .client
591 .get(url)
592 .headers(self.headers.clone())
593 .send()
594 .await?
595 .error_for_status()?;
596 let _ = response;
597 Ok(())
598 }
599 }
600 };
601 assert_eq!(actual, expected);
602 }
603}