1use itertools::Itertools;
2use ploidy_core::{
3 ir::{OperationView, RequestView, ResponseView},
4 parse::{
5 Method,
6 path::{PathFragment, PathRun},
7 },
8};
9use proc_macro2::{Span, TokenStream};
10use quote::{ToTokens, TokenStreamExt, format_ident, quote};
11use syn::Ident;
12
13use super::{
14 doc_attrs,
15 graph::{CodegenGraph, IdentMapping},
16 naming::CodegenIdentUsage,
17 ref_::CodegenRef,
18};
19
20pub struct CodegenOperation<'a> {
22 graph: &'a CodegenGraph<'a>,
23 op: &'a OperationView<'a, 'a>,
24}
25
26impl<'a> CodegenOperation<'a> {
27 pub fn new(graph: &'a CodegenGraph<'a>, op: &'a OperationView<'a, 'a>) -> Self {
28 Self { graph, op }
29 }
30
31 fn url(&self) -> TokenStream {
34 let segments = self.op.path().runs().map(|run| match run {
36 PathRun::Literals(literals) => match &*literals {
37 [one] => quote! { .push(#one) },
38 many => quote! { .extend(&[#(#many),*]) },
39 },
40 PathRun::Templated([PathFragment::Param(name)]) => {
41 let param = CodegenIdentUsage::Param(
42 self.graph.ident(IdentMapping::Path(self.op.id(), name)),
43 );
44 quote! { .push(#param) }
45 }
46 PathRun::Templated(fragments) => {
47 let format = fragments.iter().fold(String::new(), |mut f, fragment| {
49 match fragment {
50 PathFragment::Literal(text) => {
51 f.push_str(&text.replace('{', "{{").replace('}', "}}"))
52 }
53 PathFragment::Param(_) => f.push_str("{}"),
54 }
55 f
56 });
57 let args = fragments
58 .iter()
59 .filter_map(|fragment| match fragment {
60 PathFragment::Param(name) => Some(name),
61 PathFragment::Literal(_) => None,
62 })
63 .map(|name| {
64 let param = CodegenIdentUsage::Param(
68 self.graph.ident(IdentMapping::Path(self.op.id(), name)),
69 );
70 quote!(#param)
71 });
72 quote! { .push(&format!(#format, #(#args),*)) }
73 }
74 });
75
76 let pairs = self
78 .op
79 .path()
80 .query()
81 .map(|param| {
82 let name = param.name;
83 let value = param.value;
84 quote! { .append_pair(#name, #value) }
85 })
86 .reduce(|a, b| quote!(#a #b))
87 .map(|pairs| {
88 quote! {
89 url.query_pairs_mut()
90 #pairs;
91 }
92 });
93
94 let query = self.op.query().next().is_some().then(|| {
96 let query_name = format_ident!(
97 "{}Query",
98 CodegenIdentUsage::Type(self.graph.ident(self.op.id()))
99 );
100 quote! {
101 let url = ::ploidy_util::serde::Serialize::serialize(
102 query,
103 ::ploidy_util::QuerySerializer::new(
104 url,
105 parameters::#query_name::STYLES,
106 ),
107 )?;
108 }
109 });
110
111 quote! {
112 let url = {
113 let mut url = self.base_url.clone();
114 url.path_segments_mut()
115 .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
116 .pop_if_empty()
117 #(#segments)*;
118 #pairs
119 #query
120 #[cfg(feature = "tracing")]
121 {
122 ::tracing::record_all!(::tracing::Span::current(),
123 server.address = url.host_str(),
124 server.port = url.port_or_known_default(),
125 url.full = url.as_str(),
128 );
129 }
130 url
131 };
132 }
133 }
134}
135
136impl ToTokens for CodegenOperation<'_> {
137 fn to_tokens(&self, tokens: &mut TokenStream) {
138 let mut params = vec![];
139
140 let paths = self.op.path().params().collect_vec();
141 for param in &paths {
142 let param = CodegenIdentUsage::Param(
143 self.graph
144 .ident(IdentMapping::Path(self.op.id(), param.name())),
145 );
146 params.push(quote! { #param: &str });
147 }
148
149 if self.op.query().next().is_some() {
150 let query_type_name = format_ident!(
153 "{}Query",
154 CodegenIdentUsage::Type(self.graph.ident(self.op.id()))
155 );
156 params.push(quote! { query: ¶meters::#query_type_name });
157 }
158
159 if let Some(request) = self.op.request() {
160 match request {
161 RequestView::Json(view) => {
162 let param_type = CodegenRef::new(self.graph, &view);
163 params.push(quote! { request: impl Into<#param_type> });
164 }
165 RequestView::Multipart => {
166 params.push(quote! { form: crate::util::reqwest::multipart::Form });
167 }
168 }
169 }
170
171 let return_type = match self.op.response() {
172 Some(response) => match response {
173 ResponseView::Json(view) => CodegenRef::new(self.graph, &view).into_token_stream(),
174 },
175 None => quote! { () },
176 };
177
178 let url = self.url();
179
180 let request = {
181 let method = CodegenMethod(self.op.method());
182 let builder = match self.op.request() {
183 Some(RequestView::Json(_)) => quote! {
184 let request = self.client
185 .#method(url)
186 .headers(self.headers.clone())
187 .json(&request.into());
188 },
189 Some(RequestView::Multipart) => quote! {
190 let request = self.client
191 .#method(url)
192 .headers(self.headers.clone())
193 .multipart(form);
194 },
195 None => quote! {
196 let request = self.client
197 .#method(url)
198 .headers(self.headers.clone());
199 },
200 };
201 quote! {
202 let request = {
203 #builder
204 #[cfg(feature = "trace-context")]
205 let request = ::ploidy_util::trace::propagate(
206 ::tracing::Span::current(),
207 request,
208 );
209 request
210 };
211 let response = request.send().await?;
212 #[cfg(feature = "tracing")]
213 {
214 ::tracing::record_all!(::tracing::Span::current(),
215 http.response.status_code = response.status().as_u16()
216 );
217 }
218 let response = response.error_for_status()?;
219 }
220 };
221
222 let response = if self.op.response().is_some() {
223 quote! {
224 let body = response.bytes().await?;
225 let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
226 let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)?;
227 Ok(result)
228 }
229 } else {
230 quote! {
231 let _ = response;
232 Ok(())
233 }
234 };
235
236 let method_name = CodegenIdentUsage::Method(self.graph.ident(self.op.id()));
237
238 let instrument = {
239 let name = format!("{} {}", self.op.method().as_str(), self.op.path());
240 let template = self.op.path().to_string();
241 let method = self.op.method().as_str();
242 let mut fields = vec![
243 quote!(otel.name = #name),
244 quote!(otel.kind = "client"),
245 quote!(url.template = #template),
246 quote!(http.request.method = #method),
247 quote!(server.address, server.port, url.full, http.response.status_code, error.type),
248 ];
249 fields.extend(paths.iter().map(|param| {
250 let param = CodegenIdentUsage::Param(
251 self.graph
252 .ident(IdentMapping::Path(self.op.id(), param.name())),
253 );
254 quote!(#param = %#param)
255 }));
256 quote! {
257 #[cfg_attr(feature = "tracing", ::tracing::instrument(
258 skip_all,
259 fields(#(#fields),*)
260 ))]
261 }
262 };
263
264 let doc = {
265 let url = format!(" {} {}", self.op.method().as_str(), self.op.path());
266 match self.op.description() {
267 Some(description) => {
268 let attrs = doc_attrs(description);
269 quote! {
270 #attrs
271 #[doc = ""]
272 #[doc = #url]
273 }
274 }
275 None => {
276 quote!(#[doc = #url])
277 }
278 }
279 };
280
281 tokens.append_all(quote! {
282 #doc
283 #instrument
284 pub async fn #method_name(
285 &self,
286 #(#params),*
287 ) -> Result<#return_type, crate::error::Error> {
288 let result: Result<_, crate::error::Error> = async move {
289 #url
290 #request
291 #response
292 }.await;
293 #[cfg(feature = "tracing")]
294 if let Err(err) = &result {
295 ::tracing::record_all!(::tracing::Span::current(),
296 error.type = %err.category(),
297 );
298 }
299 result
300 }
301 });
302 }
303}
304
305#[derive(Clone, Copy, Debug)]
306pub struct CodegenMethod(pub Method);
307
308impl ToTokens for CodegenMethod {
309 fn to_tokens(&self, tokens: &mut TokenStream) {
310 tokens.append(match self.0 {
311 Method::Get => Ident::new("get", Span::call_site()),
312 Method::Post => Ident::new("post", Span::call_site()),
313 Method::Put => Ident::new("put", Span::call_site()),
314 Method::Patch => Ident::new("patch", Span::call_site()),
315 Method::Delete => Ident::new("delete", Span::call_site()),
316 });
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 use ploidy_core::{
325 arena::Arena,
326 ir::{RawGraph, Spec},
327 parse::Document,
328 };
329 use pretty_assertions::assert_eq;
330 use syn::parse_quote;
331
332 use crate::CodegenGraph;
333
334 #[test]
337 fn test_operation_with_path_and_query_params() {
338 let doc = Document::from_yaml(indoc::indoc! {"
339 openapi: 3.0.0
340 info:
341 title: Test API
342 version: 1.0.0
343 paths:
344 /items/{item_id}:
345 get:
346 operationId: getItem
347 description: Gets an item.
348 parameters:
349 - name: item_id
350 in: path
351 required: true
352 schema:
353 type: string
354 - name: expand
355 in: query
356 schema:
357 type: boolean
358 responses:
359 '200':
360 description: OK
361 "})
362 .unwrap();
363
364 let arena = Arena::new();
365 let spec = Spec::from_doc(&arena, &doc).unwrap();
366 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
367
368 let op = graph.operations().next().unwrap();
369 let codegen = CodegenOperation::new(&graph, &op);
370
371 let actual: syn::ImplItemFn = parse_quote!(#codegen);
372 let expected: syn::ImplItemFn = parse_quote! {
373 #[doc = " Gets an item."]
374 #[doc = ""]
375 #[doc = " GET /items/{item_id}"]
376 #[cfg_attr(
377 feature = "tracing",
378 ::tracing::instrument(
379 skip_all,
380 fields(
381 otel.name = "GET /items/{item_id}",
382 otel.kind = "client",
383 url.template = "/items/{item_id}",
384 http.request.method = "GET",
385 server.address,
386 server.port,
387 url.full,
388 http.response.status_code,
389 error.type,
390 item_id = %item_id
391 )
392 )
393 )]
394 pub async fn get_item(
395 &self,
396 item_id: &str,
397 query: ¶meters::GetItemQuery
398 ) -> Result<(), crate::error::Error> {
399 let result: Result<_, crate::error::Error> = async move {
400 let url = {
401 let mut url = self.base_url.clone();
402 url.path_segments_mut()
403 .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
404 .pop_if_empty()
405 .push("items")
406 .push(item_id);
407 let url = ::ploidy_util::serde::Serialize::serialize(
408 query,
409 ::ploidy_util::QuerySerializer::new(
410 url,
411 parameters::GetItemQuery::STYLES,
412 ),
413 )?;
414 #[cfg(feature = "tracing")]
415 {
416 ::tracing::record_all!(::tracing::Span::current(),
417 server.address = url.host_str(),
418 server.port = url.port_or_known_default(),
419 url.full = url.as_str(),
420 );
421 }
422 url
423 };
424 let request = {
425 let request = self
426 .client
427 .get(url)
428 .headers(self.headers.clone());
429 #[cfg(feature = "trace-context")]
430 let request = ::ploidy_util::trace::propagate(
431 ::tracing::Span::current(),
432 request,
433 );
434 request
435 };
436 let response = request
437 .send()
438 .await?;
439 #[cfg(feature = "tracing")]
440 {
441 ::tracing::record_all!(::tracing::Span::current(),
442 http.response.status_code = response.status().as_u16()
443 );
444 }
445 let response = response.error_for_status()?;
446 let _ = response;
447 Ok(())
448 }.await;
449 #[cfg(feature = "tracing")]
450 if let Err(err) = &result {
451 ::tracing::record_all!(::tracing::Span::current(),
452 error.type = %err.category(),
453 );
454 }
455 result
456 }
457 };
458 assert_eq!(actual, expected);
459 }
460
461 #[test]
462 fn test_operation_with_query_params_only() {
463 let doc = Document::from_yaml(indoc::indoc! {"
464 openapi: 3.0.0
465 info:
466 title: Test API
467 version: 1.0.0
468 paths:
469 /items:
470 get:
471 operationId: getItems
472 parameters:
473 - name: limit
474 in: query
475 schema:
476 type: integer
477 format: int32
478 responses:
479 '200':
480 description: OK
481 "})
482 .unwrap();
483
484 let arena = Arena::new();
485 let spec = Spec::from_doc(&arena, &doc).unwrap();
486 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
487
488 let op = graph.operations().next().unwrap();
489 let codegen = CodegenOperation::new(&graph, &op);
490
491 let actual: syn::ImplItemFn = parse_quote!(#codegen);
492 let expected: syn::ImplItemFn = parse_quote! {
493 #[doc = " GET /items"]
494 #[cfg_attr(
495 feature = "tracing",
496 ::tracing::instrument(
497 skip_all,
498 fields(
499 otel.name = "GET /items",
500 otel.kind = "client",
501 url.template = "/items",
502 http.request.method = "GET",
503 server.address,
504 server.port,
505 url.full,
506 http.response.status_code,
507 error.type
508 )
509 )
510 )]
511 pub async fn get_items(
512 &self,
513 query: ¶meters::GetItemsQuery
514 ) -> Result<(), crate::error::Error> {
515 let result: Result<_, crate::error::Error> = async move {
516 let url = {
517 let mut url = self.base_url.clone();
518 url.path_segments_mut()
519 .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
520 .pop_if_empty()
521 .push("items");
522 let url = ::ploidy_util::serde::Serialize::serialize(
523 query,
524 ::ploidy_util::QuerySerializer::new(
525 url,
526 parameters::GetItemsQuery::STYLES,
527 ),
528 )?;
529 #[cfg(feature = "tracing")]
530 {
531 ::tracing::record_all!(::tracing::Span::current(),
532 server.address = url.host_str(),
533 server.port = url.port_or_known_default(),
534 url.full = url.as_str(),
535 );
536 }
537 url
538 };
539 let request = {
540 let request = self
541 .client
542 .get(url)
543 .headers(self.headers.clone());
544 #[cfg(feature = "trace-context")]
545 let request = ::ploidy_util::trace::propagate(
546 ::tracing::Span::current(),
547 request,
548 );
549 request
550 };
551 let response = request
552 .send()
553 .await?;
554 #[cfg(feature = "tracing")]
555 {
556 ::tracing::record_all!(::tracing::Span::current(),
557 http.response.status_code = response.status().as_u16()
558 );
559 }
560 let response = response.error_for_status()?;
561 let _ = response;
562 Ok(())
563 }.await;
564 #[cfg(feature = "tracing")]
565 if let Err(err) = &result {
566 ::tracing::record_all!(::tracing::Span::current(),
567 error.type = %err.category(),
568 );
569 }
570 result
571 }
572 };
573 assert_eq!(actual, expected);
574 }
575
576 #[test]
577 fn test_path_param_named_query_does_not_shadow() {
578 let doc = Document::from_yaml(indoc::indoc! {"
579 openapi: 3.0.0
580 info:
581 title: Test API
582 version: 1.0.0
583 paths:
584 /search/{query}:
585 get:
586 operationId: search
587 parameters:
588 - name: query
589 in: path
590 required: true
591 schema:
592 type: string
593 - name: limit
594 in: query
595 schema:
596 type: integer
597 format: int32
598 responses:
599 '200':
600 description: OK
601 "})
602 .unwrap();
603
604 let arena = Arena::new();
605 let spec = Spec::from_doc(&arena, &doc).unwrap();
606 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
607
608 let op = graph.operations().next().unwrap();
609 let codegen = CodegenOperation::new(&graph, &op);
610
611 let actual: syn::ImplItemFn = parse_quote!(#codegen);
612 let expected: syn::ImplItemFn = parse_quote! {
613 #[doc = " GET /search/{query}"]
614 #[cfg_attr(
615 feature = "tracing",
616 ::tracing::instrument(
617 skip_all,
618 fields(
619 otel.name = "GET /search/{query}",
620 otel.kind = "client",
621 url.template = "/search/{query}",
622 http.request.method = "GET",
623 server.address,
624 server.port,
625 url.full,
626 http.response.status_code,
627 error.type,
628 query_2 = %query_2
629 )
630 )
631 )]
632 pub async fn search(
633 &self,
634 query_2: &str,
635 query: ¶meters::SearchQuery
636 ) -> Result<(), crate::error::Error> {
637 let result: Result<_, crate::error::Error> = async move {
638 let url = {
639 let mut url = self.base_url.clone();
640 url.path_segments_mut()
641 .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
642 .pop_if_empty()
643 .push("search")
644 .push(query_2);
645 let url = ::ploidy_util::serde::Serialize::serialize(
646 query,
647 ::ploidy_util::QuerySerializer::new(
648 url,
649 parameters::SearchQuery::STYLES,
650 ),
651 )?;
652 #[cfg(feature = "tracing")]
653 {
654 ::tracing::record_all!(::tracing::Span::current(),
655 server.address = url.host_str(),
656 server.port = url.port_or_known_default(),
657 url.full = url.as_str(),
658 );
659 }
660 url
661 };
662 let request = {
663 let request = self
664 .client
665 .get(url)
666 .headers(self.headers.clone());
667 #[cfg(feature = "trace-context")]
668 let request = ::ploidy_util::trace::propagate(
669 ::tracing::Span::current(),
670 request,
671 );
672 request
673 };
674 let response = request
675 .send()
676 .await?;
677 #[cfg(feature = "tracing")]
678 {
679 ::tracing::record_all!(::tracing::Span::current(),
680 http.response.status_code = response.status().as_u16()
681 );
682 }
683 let response = response.error_for_status()?;
684 let _ = response;
685 Ok(())
686 }.await;
687 #[cfg(feature = "tracing")]
688 if let Err(err) = &result {
689 ::tracing::record_all!(::tracing::Span::current(),
690 error.type = %err.category(),
691 );
692 }
693 result
694 }
695 };
696 assert_eq!(actual, expected);
697 }
698
699 #[test]
702 fn test_operation_with_query_params_and_request_body() {
703 let doc = Document::from_yaml(indoc::indoc! {"
704 openapi: 3.0.0
705 info:
706 title: Test API
707 version: 1.0.0
708 paths:
709 /items/{item_id}:
710 put:
711 operationId: updateItem
712 parameters:
713 - name: item_id
714 in: path
715 required: true
716 schema:
717 type: string
718 - name: dry_run
719 in: query
720 schema:
721 type: boolean
722 requestBody:
723 content:
724 application/json:
725 schema:
726 $ref: '#/components/schemas/Item'
727 responses:
728 '200':
729 description: OK
730 content:
731 application/json:
732 schema:
733 $ref: '#/components/schemas/Item'
734 components:
735 schemas:
736 Item:
737 type: object
738 properties:
739 name:
740 type: string
741 "})
742 .unwrap();
743
744 let arena = Arena::new();
745 let spec = Spec::from_doc(&arena, &doc).unwrap();
746 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
747
748 let op = graph.operations().next().unwrap();
749 let codegen = CodegenOperation::new(&graph, &op);
750
751 let actual: syn::ImplItemFn = parse_quote!(#codegen);
752 let expected: syn::ImplItemFn = parse_quote! {
753 #[doc = " PUT /items/{item_id}"]
754 #[cfg_attr(
755 feature = "tracing",
756 ::tracing::instrument(
757 skip_all,
758 fields(
759 otel.name = "PUT /items/{item_id}",
760 otel.kind = "client",
761 url.template = "/items/{item_id}",
762 http.request.method = "PUT",
763 server.address,
764 server.port,
765 url.full,
766 http.response.status_code,
767 error.type,
768 item_id = %item_id
769 )
770 )
771 )]
772 pub async fn update_item(
773 &self,
774 item_id: &str,
775 query: ¶meters::UpdateItemQuery,
776 request: impl Into<crate::types::Item>
777 ) -> Result<crate::types::Item, crate::error::Error> {
778 let result: Result<_, crate::error::Error> = async move {
779 let url = {
780 let mut url = self.base_url.clone();
781 url.path_segments_mut()
782 .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
783 .pop_if_empty()
784 .push("items")
785 .push(item_id);
786 let url = ::ploidy_util::serde::Serialize::serialize(
787 query,
788 ::ploidy_util::QuerySerializer::new(
789 url,
790 parameters::UpdateItemQuery::STYLES,
791 ),
792 )?;
793 #[cfg(feature = "tracing")]
794 {
795 ::tracing::record_all!(::tracing::Span::current(),
796 server.address = url.host_str(),
797 server.port = url.port_or_known_default(),
798 url.full = url.as_str(),
799 );
800 }
801 url
802 };
803 let request = {
804 let request = self
805 .client
806 .put(url)
807 .headers(self.headers.clone())
808 .json(&request.into());
809 #[cfg(feature = "trace-context")]
810 let request = ::ploidy_util::trace::propagate(
811 ::tracing::Span::current(),
812 request,
813 );
814 request
815 };
816 let response = request
817 .send()
818 .await?;
819 #[cfg(feature = "tracing")]
820 {
821 ::tracing::record_all!(::tracing::Span::current(),
822 http.response.status_code = response.status().as_u16()
823 );
824 }
825 let response = response.error_for_status()?;
826 let body = response.bytes().await?;
827 let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
828 let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)?;
829 Ok(result)
830 }.await;
831 #[cfg(feature = "tracing")]
832 if let Err(err) = &result {
833 ::tracing::record_all!(::tracing::Span::current(),
834 error.type = %err.category(),
835 );
836 }
837 result
838 }
839 };
840 assert_eq!(actual, expected);
841 }
842
843 #[test]
846 fn test_operation_without_query_params() {
847 let doc = Document::from_yaml(indoc::indoc! {"
848 openapi: 3.0.0
849 info:
850 title: Test API
851 version: 1.0.0
852 paths:
853 /items/{item_id}:
854 get:
855 operationId: getItem
856 parameters:
857 - name: item_id
858 in: path
859 required: true
860 schema:
861 type: string
862 responses:
863 '200':
864 description: OK
865 "})
866 .unwrap();
867
868 let arena = Arena::new();
869 let spec = Spec::from_doc(&arena, &doc).unwrap();
870 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
871
872 let op = graph.operations().next().unwrap();
873 let codegen = CodegenOperation::new(&graph, &op);
874
875 let actual: syn::ImplItemFn = parse_quote!(#codegen);
876 let expected: syn::ImplItemFn = parse_quote! {
877 #[doc = " GET /items/{item_id}"]
878 #[cfg_attr(
879 feature = "tracing",
880 ::tracing::instrument(
881 skip_all,
882 fields(
883 otel.name = "GET /items/{item_id}",
884 otel.kind = "client",
885 url.template = "/items/{item_id}",
886 http.request.method = "GET",
887 server.address,
888 server.port,
889 url.full,
890 http.response.status_code,
891 error.type,
892 item_id = %item_id
893 )
894 )
895 )]
896 pub async fn get_item(
897 &self,
898 item_id: &str
899 ) -> Result<(), crate::error::Error> {
900 let result: Result<_, crate::error::Error> = async move {
901 let url = {
902 let mut url = self.base_url.clone();
903 url.path_segments_mut()
904 .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
905 .pop_if_empty()
906 .push("items")
907 .push(item_id);
908 #[cfg(feature = "tracing")]
909 {
910 ::tracing::record_all!(::tracing::Span::current(),
911 server.address = url.host_str(),
912 server.port = url.port_or_known_default(),
913 url.full = url.as_str(),
914 );
915 }
916 url
917 };
918 let request = {
919 let request = self
920 .client
921 .get(url)
922 .headers(self.headers.clone());
923 #[cfg(feature = "trace-context")]
924 let request = ::ploidy_util::trace::propagate(
925 ::tracing::Span::current(),
926 request,
927 );
928 request
929 };
930 let response = request
931 .send()
932 .await?;
933 #[cfg(feature = "tracing")]
934 {
935 ::tracing::record_all!(::tracing::Span::current(),
936 http.response.status_code = response.status().as_u16()
937 );
938 }
939 let response = response.error_for_status()?;
940 let _ = response;
941 Ok(())
942 }.await;
943 #[cfg(feature = "tracing")]
944 if let Err(err) = &result {
945 ::tracing::record_all!(::tracing::Span::current(),
946 error.type = %err.category(),
947 );
948 }
949 result
950 }
951 };
952 assert_eq!(actual, expected);
953 }
954
955 #[test]
958 fn test_operation_with_synthesized_path_param() {
959 let doc = Document::from_yaml(indoc::indoc! {"
960 openapi: 3.0.0
961 info:
962 title: Test API
963 version: 1.0.0
964 paths:
965 /items/{item_id}:
966 get:
967 operationId: getItem
968 responses:
969 '200':
970 description: OK
971 "})
972 .unwrap();
973
974 let arena = Arena::new();
975 let spec = Spec::from_doc(&arena, &doc).unwrap();
976 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
977
978 let op = graph.operations().next().unwrap();
979 let codegen = CodegenOperation::new(&graph, &op);
980
981 let actual: syn::ImplItemFn = parse_quote!(#codegen);
982 let expected: syn::ImplItemFn = parse_quote! {
983 #[doc = " GET /items/{item_id}"]
984 #[cfg_attr(
985 feature = "tracing",
986 ::tracing::instrument(
987 skip_all,
988 fields(
989 otel.name = "GET /items/{item_id}",
990 otel.kind = "client",
991 url.template = "/items/{item_id}",
992 http.request.method = "GET",
993 server.address,
994 server.port,
995 url.full,
996 http.response.status_code,
997 error.type,
998 item_id = %item_id
999 )
1000 )
1001 )]
1002 pub async fn get_item(
1003 &self,
1004 item_id: &str
1005 ) -> Result<(), crate::error::Error> {
1006 let result: Result<_, crate::error::Error> = async move {
1007 let url = {
1008 let mut url = self.base_url.clone();
1009 url.path_segments_mut()
1010 .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
1011 .pop_if_empty()
1012 .push("items")
1013 .push(item_id);
1014 #[cfg(feature = "tracing")]
1015 {
1016 ::tracing::record_all!(::tracing::Span::current(),
1017 server.address = url.host_str(),
1018 server.port = url.port_or_known_default(),
1019 url.full = url.as_str(),
1020 );
1021 }
1022 url
1023 };
1024 let request = {
1025 let request = self
1026 .client
1027 .get(url)
1028 .headers(self.headers.clone());
1029 #[cfg(feature = "trace-context")]
1030 let request = ::ploidy_util::trace::propagate(
1031 ::tracing::Span::current(),
1032 request,
1033 );
1034 request
1035 };
1036 let response = request
1037 .send()
1038 .await?;
1039 #[cfg(feature = "tracing")]
1040 {
1041 ::tracing::record_all!(::tracing::Span::current(),
1042 http.response.status_code = response.status().as_u16()
1043 );
1044 }
1045 let response = response.error_for_status()?;
1046 let _ = response;
1047 Ok(())
1048 }.await;
1049 #[cfg(feature = "tracing")]
1050 if let Err(err) = &result {
1051 ::tracing::record_all!(::tracing::Span::current(),
1052 error.type = %err.category(),
1053 );
1054 }
1055 result
1056 }
1057 };
1058 assert_eq!(actual, expected);
1059 }
1060
1061 #[test]
1064 fn test_operation_with_literal_query_params() {
1065 let doc = Document::from_yaml(indoc::indoc! {"
1066 openapi: 3.0.0
1067 info:
1068 title: Test API
1069 version: 1.0.0
1070 paths:
1071 /v1/messages?beta=true&expand:
1072 post:
1073 operationId: betaCreateMessage
1074 requestBody:
1075 content:
1076 application/json:
1077 schema:
1078 $ref: '#/components/schemas/Message'
1079 responses:
1080 '200':
1081 description: OK
1082 content:
1083 application/json:
1084 schema:
1085 $ref: '#/components/schemas/Message'
1086 components:
1087 schemas:
1088 Message:
1089 type: object
1090 properties:
1091 content:
1092 type: string
1093 "})
1094 .unwrap();
1095
1096 let arena = Arena::new();
1097 let spec = Spec::from_doc(&arena, &doc).unwrap();
1098 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1099
1100 let op = graph.operations().next().unwrap();
1101 let codegen = CodegenOperation::new(&graph, &op);
1102
1103 let actual: syn::ImplItemFn = parse_quote!(#codegen);
1104 let expected: syn::ImplItemFn = parse_quote! {
1105 #[doc = " POST /v1/messages?beta=true&expand="]
1106 #[cfg_attr(
1107 feature = "tracing",
1108 ::tracing::instrument(
1109 skip_all,
1110 fields(
1111 otel.name = "POST /v1/messages?beta=true&expand=",
1112 otel.kind = "client",
1113 url.template = "/v1/messages?beta=true&expand=",
1114 http.request.method = "POST",
1115 server.address,
1116 server.port,
1117 url.full,
1118 http.response.status_code,
1119 error.type
1120 )
1121 )
1122 )]
1123 pub async fn beta_create_message(
1124 &self,
1125 request: impl Into<crate::types::Message>
1126 ) -> Result<crate::types::Message, crate::error::Error> {
1127 let result: Result<_, crate::error::Error> = async move {
1128 let url = {
1129 let mut url = self.base_url.clone();
1130 url.path_segments_mut()
1131 .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
1132 .pop_if_empty()
1133 .extend(&["v1", "messages"]);
1134 url.query_pairs_mut()
1135 .append_pair("beta", "true")
1136 .append_pair("expand", "");
1137 #[cfg(feature = "tracing")]
1138 {
1139 ::tracing::record_all!(::tracing::Span::current(),
1140 server.address = url.host_str(),
1141 server.port = url.port_or_known_default(),
1142 url.full = url.as_str(),
1143 );
1144 }
1145 url
1146 };
1147 let request = {
1148 let request = self
1149 .client
1150 .post(url)
1151 .headers(self.headers.clone())
1152 .json(&request.into());
1153 #[cfg(feature = "trace-context")]
1154 let request = ::ploidy_util::trace::propagate(
1155 ::tracing::Span::current(),
1156 request,
1157 );
1158 request
1159 };
1160 let response = request
1161 .send()
1162 .await?;
1163 #[cfg(feature = "tracing")]
1164 {
1165 ::tracing::record_all!(::tracing::Span::current(),
1166 http.response.status_code = response.status().as_u16()
1167 );
1168 }
1169 let response = response.error_for_status()?;
1170 let body = response.bytes().await?;
1171 let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
1172 let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)?;
1173 Ok(result)
1174 }.await;
1175 #[cfg(feature = "tracing")]
1176 if let Err(err) = &result {
1177 ::tracing::record_all!(::tracing::Span::current(),
1178 error.type = %err.category(),
1179 );
1180 }
1181 result
1182 }
1183 };
1184 assert_eq!(actual, expected);
1185 }
1186
1187 #[test]
1188 fn test_operation_with_literal_and_declared_query_params() {
1189 let doc = Document::from_yaml(indoc::indoc! {"
1190 openapi: 3.0.0
1191 info:
1192 title: Test API
1193 version: 1.0.0
1194 paths:
1195 /v1/messages?beta=true:
1196 post:
1197 operationId: betaCreateMessage
1198 parameters:
1199 - name: limit
1200 in: query
1201 schema:
1202 type: integer
1203 format: int32
1204 requestBody:
1205 content:
1206 application/json:
1207 schema:
1208 $ref: '#/components/schemas/Message'
1209 responses:
1210 '200':
1211 description: OK
1212 content:
1213 application/json:
1214 schema:
1215 $ref: '#/components/schemas/Message'
1216 components:
1217 schemas:
1218 Message:
1219 type: object
1220 properties:
1221 content:
1222 type: string
1223 "})
1224 .unwrap();
1225
1226 let arena = Arena::new();
1227 let spec = Spec::from_doc(&arena, &doc).unwrap();
1228 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1229
1230 let op = graph.operations().next().unwrap();
1231 let codegen = CodegenOperation::new(&graph, &op);
1232
1233 let actual: syn::ImplItemFn = parse_quote!(#codegen);
1234 let expected: syn::ImplItemFn = parse_quote! {
1235 #[doc = " POST /v1/messages?beta=true"]
1236 #[cfg_attr(
1237 feature = "tracing",
1238 ::tracing::instrument(
1239 skip_all,
1240 fields(
1241 otel.name = "POST /v1/messages?beta=true",
1242 otel.kind = "client",
1243 url.template = "/v1/messages?beta=true",
1244 http.request.method = "POST",
1245 server.address,
1246 server.port,
1247 url.full,
1248 http.response.status_code,
1249 error.type
1250 )
1251 )
1252 )]
1253 pub async fn beta_create_message(
1254 &self,
1255 query: ¶meters::BetaCreateMessageQuery,
1256 request: impl Into<crate::types::Message>
1257 ) -> Result<crate::types::Message, crate::error::Error> {
1258 let result: Result<_, crate::error::Error> = async move {
1259 let url = {
1260 let mut url = self.base_url.clone();
1261 url.path_segments_mut()
1262 .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
1263 .pop_if_empty()
1264 .extend(&["v1", "messages"]);
1265 url.query_pairs_mut()
1266 .append_pair("beta", "true");
1267 let url = ::ploidy_util::serde::Serialize::serialize(
1268 query,
1269 ::ploidy_util::QuerySerializer::new(
1270 url,
1271 parameters::BetaCreateMessageQuery::STYLES,
1272 ),
1273 )?;
1274 #[cfg(feature = "tracing")]
1275 {
1276 ::tracing::record_all!(::tracing::Span::current(),
1277 server.address = url.host_str(),
1278 server.port = url.port_or_known_default(),
1279 url.full = url.as_str(),
1280 );
1281 }
1282 url
1283 };
1284 let request = {
1285 let request = self
1286 .client
1287 .post(url)
1288 .headers(self.headers.clone())
1289 .json(&request.into());
1290 #[cfg(feature = "trace-context")]
1291 let request = ::ploidy_util::trace::propagate(
1292 ::tracing::Span::current(),
1293 request,
1294 );
1295 request
1296 };
1297 let response = request
1298 .send()
1299 .await?;
1300 #[cfg(feature = "tracing")]
1301 {
1302 ::tracing::record_all!(::tracing::Span::current(),
1303 http.response.status_code = response.status().as_u16()
1304 );
1305 }
1306 let response = response.error_for_status()?;
1307 let body = response.bytes().await?;
1308 let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
1309 let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)?;
1310 Ok(result)
1311 }.await;
1312 #[cfg(feature = "tracing")]
1313 if let Err(err) = &result {
1314 ::tracing::record_all!(::tracing::Span::current(),
1315 error.type = %err.category(),
1316 );
1317 }
1318 result
1319 }
1320 };
1321 assert_eq!(actual, expected);
1322 }
1323
1324 #[test]
1325 fn test_operation_with_path_params_and_literal_and_declared_query_params() {
1326 let doc = Document::from_yaml(indoc::indoc! {"
1327 openapi: 3.0.0
1328 info:
1329 title: Test API
1330 version: 1.0.0
1331 paths:
1332 /v1/models/{model_id}?beta=true:
1333 get:
1334 operationId: betaGetModel
1335 parameters:
1336 - name: model_id
1337 in: path
1338 required: true
1339 schema:
1340 type: string
1341 - name: expand
1342 in: query
1343 schema:
1344 type: boolean
1345 responses:
1346 '200':
1347 description: OK
1348 content:
1349 application/json:
1350 schema:
1351 $ref: '#/components/schemas/Model'
1352 components:
1353 schemas:
1354 Model:
1355 type: object
1356 properties:
1357 id:
1358 type: string
1359 "})
1360 .unwrap();
1361
1362 let arena = Arena::new();
1363 let spec = Spec::from_doc(&arena, &doc).unwrap();
1364 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1365
1366 let op = graph.operations().next().unwrap();
1367 let codegen = CodegenOperation::new(&graph, &op);
1368
1369 let actual: syn::ImplItemFn = parse_quote!(#codegen);
1370 let expected: syn::ImplItemFn = parse_quote! {
1371 #[doc = " GET /v1/models/{model_id}?beta=true"]
1372 #[cfg_attr(
1373 feature = "tracing",
1374 ::tracing::instrument(
1375 skip_all,
1376 fields(
1377 otel.name = "GET /v1/models/{model_id}?beta=true",
1378 otel.kind = "client",
1379 url.template = "/v1/models/{model_id}?beta=true",
1380 http.request.method = "GET",
1381 server.address,
1382 server.port,
1383 url.full,
1384 http.response.status_code,
1385 error.type,
1386 model_id = %model_id
1387 )
1388 )
1389 )]
1390 pub async fn beta_get_model(
1391 &self,
1392 model_id: &str,
1393 query: ¶meters::BetaGetModelQuery
1394 ) -> Result<crate::types::Model, crate::error::Error> {
1395 let result: Result<_, crate::error::Error> = async move {
1396 let url = {
1397 let mut url = self.base_url.clone();
1398 url.path_segments_mut()
1399 .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
1400 .pop_if_empty()
1401 .extend(&["v1", "models"])
1402 .push(model_id);
1403 url.query_pairs_mut()
1404 .append_pair("beta", "true");
1405 let url = ::ploidy_util::serde::Serialize::serialize(
1406 query,
1407 ::ploidy_util::QuerySerializer::new(
1408 url,
1409 parameters::BetaGetModelQuery::STYLES,
1410 ),
1411 )?;
1412 #[cfg(feature = "tracing")]
1413 {
1414 ::tracing::record_all!(::tracing::Span::current(),
1415 server.address = url.host_str(),
1416 server.port = url.port_or_known_default(),
1417 url.full = url.as_str(),
1418 );
1419 }
1420 url
1421 };
1422 let request = {
1423 let request = self
1424 .client
1425 .get(url)
1426 .headers(self.headers.clone());
1427 #[cfg(feature = "trace-context")]
1428 let request = ::ploidy_util::trace::propagate(
1429 ::tracing::Span::current(),
1430 request,
1431 );
1432 request
1433 };
1434 let response = request
1435 .send()
1436 .await?;
1437 #[cfg(feature = "tracing")]
1438 {
1439 ::tracing::record_all!(::tracing::Span::current(),
1440 http.response.status_code = response.status().as_u16()
1441 );
1442 }
1443 let response = response.error_for_status()?;
1444 let body = response.bytes().await?;
1445 let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
1446 let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)?;
1447 Ok(result)
1448 }.await;
1449 #[cfg(feature = "tracing")]
1450 if let Err(err) = &result {
1451 ::tracing::record_all!(::tracing::Span::current(),
1452 error.type = %err.category(),
1453 );
1454 }
1455 result
1456 }
1457 };
1458 assert_eq!(actual, expected);
1459 }
1460}