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 pub async fn list_customers(
189 &self,
190 ) -> Result<::std::vec::Vec<crate::types::Customer>, crate::error::Error> {
191 let url = {
192 let mut url = self.base_url.clone();
193 let _ = url
194 .path_segments_mut()
195 .map(|mut segments| {
196 segments.pop_if_empty()
197 .push("customers");
198 });
199 url
200 };
201 let response = self
202 .client
203 .get(url)
204 .headers(self.headers.clone())
205 .send()
206 .await?
207 .error_for_status()?;
208 let body = response.bytes().await?;
209 let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
210 let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)
211 .map_err(crate::error::JsonError::from)?;
212 Ok(result)
213 }
214 }
215 };
216 assert_eq!(actual, expected);
217 }
218
219 #[test]
220 fn test_operation_method_with_named_deps_has_cfg() {
221 let doc = Document::from_yaml(indoc::indoc! {"
222 openapi: 3.0.0
223 info:
224 title: Test
225 version: 1.0.0
226 paths:
227 /orders:
228 get:
229 operationId: listOrders
230 x-resource-name: orders
231 responses:
232 '200':
233 description: OK
234 content:
235 application/json:
236 schema:
237 type: array
238 items:
239 $ref: '#/components/schemas/Order'
240 components:
241 schemas:
242 Order:
243 type: object
244 properties:
245 customer:
246 $ref: '#/components/schemas/Customer'
247 Customer:
248 type: object
249 x-resourceId: customer
250 properties:
251 id:
252 type: string
253 "})
254 .unwrap();
255
256 let arena = Arena::new();
257 let spec = Spec::from_doc(&arena, &doc).unwrap();
258 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
259
260 let ops = graph.operations().collect_vec();
261 let [op] = &*ops else {
262 panic!("expected one operation; got `{ops:?}`");
263 };
264 let resource =
265 CodegenResource::new(&graph, graph.resource_for(op), std::slice::from_ref(op));
266
267 let actual: syn::File = parse_quote!(#resource);
270 let expected: syn::File = parse_quote! {
271 impl crate::client::Client {
272 #[cfg(feature = "customer")]
273 pub async fn list_orders(
274 &self,
275 ) -> Result<::std::vec::Vec<crate::types::Order>, crate::error::Error> {
276 let url = {
277 let mut url = self.base_url.clone();
278 let _ = url
279 .path_segments_mut()
280 .map(|mut segments| {
281 segments.pop_if_empty()
282 .push("orders");
283 });
284 url
285 };
286 let response = self
287 .client
288 .get(url)
289 .headers(self.headers.clone())
290 .send()
291 .await?
292 .error_for_status()?;
293 let body = response.bytes().await?;
294 let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
295 let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)
296 .map_err(crate::error::JsonError::from)?;
297 Ok(result)
298 }
299 }
300 };
301 assert_eq!(actual, expected);
302 }
303
304 #[test]
307 fn test_resource_emits_parameters_module() {
308 let doc = Document::from_yaml(indoc::indoc! {"
309 openapi: 3.0.0
310 info:
311 title: Test
312 version: 1.0.0
313 paths:
314 /customers:
315 get:
316 operationId: listCustomers
317 x-resource-name: customer
318 parameters:
319 - name: limit
320 in: query
321 schema:
322 type: integer
323 format: int32
324 responses:
325 '200':
326 description: OK
327 "})
328 .unwrap();
329
330 let arena = Arena::new();
331 let spec = Spec::from_doc(&arena, &doc).unwrap();
332 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
333
334 let ops = graph.operations().collect_vec();
335 let [op] = &*ops else {
336 panic!("expected one operation; got `{ops:?}`");
337 };
338 let resource =
339 CodegenResource::new(&graph, graph.resource_for(op), std::slice::from_ref(op));
340
341 let actual: syn::File = parse_quote!(#resource);
342 let expected: syn::File = parse_quote! {
343 impl crate::client::Client {
344 pub async fn list_customers(
345 &self,
346 query: ¶meters::ListCustomersQuery
347 ) -> Result<(), crate::error::Error> {
348 let url = {
349 let mut url = self.base_url.clone();
350 let _ = url
351 .path_segments_mut()
352 .map(|mut segments| {
353 segments.pop_if_empty()
354 .push("customers");
355 });
356 url
357 };
358 let url = ::ploidy_util::serde::Serialize::serialize(
359 query,
360 ::ploidy_util::QuerySerializer::new(
361 url,
362 parameters::ListCustomersQuery::STYLES,
363 ),
364 )?;
365 let response = self
366 .client
367 .get(url)
368 .headers(self.headers.clone())
369 .send()
370 .await?
371 .error_for_status()?;
372 let _ = response;
373 Ok(())
374 }
375 }
376 pub mod parameters {
377 mod list_customers_query {
378 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
379 #[serde(crate = "::ploidy_util::serde")]
380 pub struct ListCustomersQuery {
381 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub limit: ::std::option::Option<i32>,
383 }
384 impl ListCustomersQuery {
385 pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[];
386 }
387 }
388 pub use list_customers_query::*;
389 }
390 };
391 assert_eq!(actual, expected);
392 }
393
394 #[test]
395 fn test_resource_with_multiple_query_ops_shares_parameters_module() {
396 let doc = Document::from_yaml(indoc::indoc! {"
397 openapi: 3.0.0
398 info:
399 title: Test
400 version: 1.0.0
401 paths:
402 /customers:
403 get:
404 operationId: listCustomers
405 x-resource-name: customer
406 parameters:
407 - name: limit
408 in: query
409 schema:
410 type: integer
411 format: int32
412 responses:
413 '200':
414 description: OK
415 /customers/search:
416 get:
417 operationId: searchCustomers
418 x-resource-name: customer
419 parameters:
420 - name: email
421 in: query
422 required: true
423 schema:
424 type: string
425 responses:
426 '200':
427 description: OK
428 "})
429 .unwrap();
430
431 let arena = Arena::new();
432 let spec = Spec::from_doc(&arena, &doc).unwrap();
433 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
434
435 let ops = graph.operations().collect_vec();
436 let resource = ops
437 .iter()
438 .map(|op| graph.resource_for(op))
439 .all_equal_value()
440 .unwrap();
441 let resource = CodegenResource::new(&graph, resource, &ops);
442
443 let actual: syn::File = parse_quote!(#resource);
444 let expected: syn::File = parse_quote! {
445 impl crate::client::Client {
446 pub async fn list_customers(
447 &self,
448 query: ¶meters::ListCustomersQuery
449 ) -> Result<(), crate::error::Error> {
450 let url = {
451 let mut url = self.base_url.clone();
452 let _ = url
453 .path_segments_mut()
454 .map(|mut segments| {
455 segments.pop_if_empty()
456 .push("customers");
457 });
458 url
459 };
460 let url = ::ploidy_util::serde::Serialize::serialize(
461 query,
462 ::ploidy_util::QuerySerializer::new(
463 url,
464 parameters::ListCustomersQuery::STYLES,
465 ),
466 )?;
467 let response = self
468 .client
469 .get(url)
470 .headers(self.headers.clone())
471 .send()
472 .await?
473 .error_for_status()?;
474 let _ = response;
475 Ok(())
476 }
477 pub async fn search_customers(
478 &self,
479 query: ¶meters::SearchCustomersQuery
480 ) -> Result<(), crate::error::Error> {
481 let url = {
482 let mut url = self.base_url.clone();
483 let _ = url
484 .path_segments_mut()
485 .map(|mut segments| {
486 segments.pop_if_empty()
487 .push("customers")
488 .push("search");
489 });
490 url
491 };
492 let url = ::ploidy_util::serde::Serialize::serialize(
493 query,
494 ::ploidy_util::QuerySerializer::new(
495 url,
496 parameters::SearchCustomersQuery::STYLES,
497 ),
498 )?;
499 let response = self
500 .client
501 .get(url)
502 .headers(self.headers.clone())
503 .send()
504 .await?
505 .error_for_status()?;
506 let _ = response;
507 Ok(())
508 }
509 }
510 pub mod parameters {
511 mod list_customers_query {
512 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
513 #[serde(crate = "::ploidy_util::serde")]
514 pub struct ListCustomersQuery {
515 #[serde(default, skip_serializing_if = "Option::is_none")]
516 pub limit: ::std::option::Option<i32>,
517 }
518 impl ListCustomersQuery {
519 pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[];
520 }
521 }
522 pub use list_customers_query::*;
523 mod search_customers_query {
524 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
525 #[serde(crate = "::ploidy_util::serde")]
526 pub struct SearchCustomersQuery {
527 pub email: ::std::string::String,
528 }
529 impl SearchCustomersQuery {
530 pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[];
531 }
532 }
533 pub use search_customers_query::*;
534 }
535 };
536 assert_eq!(actual, expected);
537 }
538
539 #[test]
540 fn test_resource_omits_parameters_module_when_no_query_params() {
541 let doc = Document::from_yaml(indoc::indoc! {"
542 openapi: 3.0.0
543 info:
544 title: Test
545 version: 1.0.0
546 paths:
547 /customers:
548 get:
549 operationId: listCustomers
550 x-resource-name: customer
551 responses:
552 '200':
553 description: OK
554 "})
555 .unwrap();
556
557 let arena = Arena::new();
558 let spec = Spec::from_doc(&arena, &doc).unwrap();
559 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
560
561 let ops = graph.operations().collect_vec();
562 let [op] = &*ops else {
563 panic!("expected one operation; got `{ops:?}`");
564 };
565 let resource =
566 CodegenResource::new(&graph, graph.resource_for(op), std::slice::from_ref(op));
567
568 let actual: syn::File = parse_quote!(#resource);
569 let expected: syn::File = parse_quote! {
570 impl crate::client::Client {
571 pub async fn list_customers(
572 &self,
573 ) -> Result<(), crate::error::Error> {
574 let url = {
575 let mut url = self.base_url.clone();
576 let _ = url
577 .path_segments_mut()
578 .map(|mut segments| {
579 segments.pop_if_empty()
580 .push("customers");
581 });
582 url
583 };
584 let response = self
585 .client
586 .get(url)
587 .headers(self.headers.clone())
588 .send()
589 .await?
590 .error_for_status()?;
591 let _ = response;
592 Ok(())
593 }
594 }
595 };
596 assert_eq!(actual, expected);
597 }
598}