Skip to main content

ploidy_codegen_rust/
operation.rs

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
20/// Generates a single client method for an API operation.
21pub 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    /// Generates code to build and interpolate path parameters into
32    /// the request URL.
33    fn url(&self) -> TokenStream {
34        let segments = self.op.path().runs().map(|run| match run {
35            PathRun::Literals(literals) => match &*literals {
36                [one] => quote! { .push(#one) },
37                many => quote! { .extend(&[#(#many),*]) },
38            },
39            PathRun::Templated([PathFragment::Param(name)]) => {
40                let param = CodegenIdentUsage::Param(
41                    self.graph.ident(IdentMapping::Path(self.op.id(), name)),
42                );
43                quote! { .push(#param) }
44            }
45            PathRun::Templated(fragments) => {
46                // Build a format string, with placeholders for parameter fragments.
47                let format = fragments.iter().fold(String::new(), |mut f, fragment| {
48                    match fragment {
49                        PathFragment::Literal(text) => {
50                            f.push_str(&text.replace('{', "{{").replace('}', "}}"))
51                        }
52                        PathFragment::Param(_) => f.push_str("{}"),
53                    }
54                    f
55                });
56                let args = fragments
57                    .iter()
58                    .filter_map(|fragment| match fragment {
59                        PathFragment::Param(name) => Some(name),
60                        PathFragment::Literal(_) => None,
61                    })
62                    .map(|name| {
63                        // `url::PathSegmentsMut::push` percent-encodes the
64                        // full segment, so we can interpolate fragments
65                        // directly.
66                        let param = CodegenIdentUsage::Param(
67                            self.graph.ident(IdentMapping::Path(self.op.id(), name)),
68                        );
69                        quote!(#param)
70                    });
71                quote! { .push(&format!(#format, #(#args),*)) }
72            }
73        });
74
75        let query = self
76            .op
77            .path()
78            .query()
79            .map(|param| {
80                let name = param.name;
81                let value = param.value;
82                quote! { .append_pair(#name, #value) }
83            })
84            .reduce(|a, b| quote!(#a #b))
85            .map(|pairs| {
86                quote! {
87                    url.query_pairs_mut()
88                        #pairs;
89                }
90            });
91
92        quote! {
93            let url = {
94                let mut url = self.base_url.clone();
95                let _ = url
96                    .path_segments_mut()
97                    .map(|mut segments| {
98                        segments.pop_if_empty()
99                            #(#segments)*;
100                    });
101                #query
102                url
103            };
104        }
105    }
106
107    /// Generates code to serialize query parameters into the URL.
108    fn query(&self) -> Option<TokenStream> {
109        self.op.query().next().is_some().then(|| {
110            let query_name = format_ident!(
111                "{}Query",
112                CodegenIdentUsage::Type(self.graph.ident(self.op.id()))
113            );
114            quote! {
115                let url = ::ploidy_util::serde::Serialize::serialize(
116                    query,
117                    ::ploidy_util::QuerySerializer::new(
118                        url,
119                        parameters::#query_name::STYLES,
120                    ),
121                )?;
122            }
123        })
124    }
125}
126
127impl ToTokens for CodegenOperation<'_> {
128    fn to_tokens(&self, tokens: &mut TokenStream) {
129        let mut params = vec![];
130
131        let paths = self.op.path().params().collect_vec();
132        for param in &paths {
133            let param = CodegenIdentUsage::Param(
134                self.graph
135                    .ident(IdentMapping::Path(self.op.id(), param.name())),
136            );
137            params.push(quote! { #param: &str });
138        }
139
140        if self.op.query().next().is_some() {
141            // Include the `query` argument if we have
142            // at least one query parameter.
143            let query_type_name = format_ident!(
144                "{}Query",
145                CodegenIdentUsage::Type(self.graph.ident(self.op.id()))
146            );
147            params.push(quote! { query: &parameters::#query_type_name });
148        }
149
150        if let Some(request) = self.op.request() {
151            match request {
152                RequestView::Json(view) => {
153                    let param_type = CodegenRef::new(self.graph, &view);
154                    params.push(quote! { request: impl Into<#param_type> });
155                }
156                RequestView::Multipart => {
157                    params.push(quote! { form: crate::util::reqwest::multipart::Form });
158                }
159            }
160        }
161
162        let return_type = match self.op.response() {
163            Some(response) => match response {
164                ResponseView::Json(view) => CodegenRef::new(self.graph, &view).into_token_stream(),
165            },
166            None => quote! { () },
167        };
168
169        let build_url = self.url();
170
171        let build_query = self.query();
172
173        let http_method = CodegenMethod(self.op.method());
174
175        let build_request = match self.op.request() {
176            Some(RequestView::Json(_)) => quote! {
177                let response = self.client
178                    .#http_method(url)
179                    .headers(self.headers.clone())
180                    .json(&request.into())
181                    .send()
182                    .await?
183                    .error_for_status()?;
184            },
185            Some(RequestView::Multipart) => quote! {
186                let response = self.client
187                    .#http_method(url)
188                    .headers(self.headers.clone())
189                    .multipart(form)
190                    .send()
191                    .await?
192                    .error_for_status()?;
193            },
194            None => quote! {
195                let response = self.client
196                    .#http_method(url)
197                    .headers(self.headers.clone())
198                    .send()
199                    .await?
200                    .error_for_status()?;
201            },
202        };
203
204        let parse_response = if self.op.response().is_some() {
205            quote! {
206                let body = response.bytes().await?;
207                let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
208                let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)
209                    .map_err(crate::error::JsonError::from)?;
210                Ok(result)
211            }
212        } else {
213            quote! {
214                let _ = response;
215                Ok(())
216            }
217        };
218
219        let method_name = CodegenIdentUsage::Method(self.graph.ident(self.op.id()));
220
221        let doc = {
222            let url = format!(" {} {}", self.op.method().as_str(), self.op.path());
223            match self.op.description() {
224                Some(description) => {
225                    let attrs = doc_attrs(description);
226                    quote! {
227                        #attrs
228                        #[doc = ""]
229                        #[doc = #url]
230                    }
231                }
232                None => {
233                    quote!(#[doc = #url])
234                }
235            }
236        };
237
238        tokens.append_all(quote! {
239            #doc
240            pub async fn #method_name(
241                &self,
242                #(#params),*
243            ) -> Result<#return_type, crate::error::Error> {
244                #build_url
245                #build_query
246                #build_request
247                #parse_response
248            }
249        });
250    }
251}
252
253#[derive(Clone, Copy, Debug)]
254pub struct CodegenMethod(pub Method);
255
256impl ToTokens for CodegenMethod {
257    fn to_tokens(&self, tokens: &mut TokenStream) {
258        tokens.append(match self.0 {
259            Method::Get => Ident::new("get", Span::call_site()),
260            Method::Post => Ident::new("post", Span::call_site()),
261            Method::Put => Ident::new("put", Span::call_site()),
262            Method::Patch => Ident::new("patch", Span::call_site()),
263            Method::Delete => Ident::new("delete", Span::call_site()),
264        });
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    use ploidy_core::{
273        arena::Arena,
274        ir::{RawGraph, Spec},
275        parse::Document,
276    };
277    use pretty_assertions::assert_eq;
278    use syn::parse_quote;
279
280    use crate::CodegenGraph;
281
282    // MARK: With query params
283
284    #[test]
285    fn test_operation_with_path_and_query_params() {
286        let doc = Document::from_yaml(indoc::indoc! {"
287            openapi: 3.0.0
288            info:
289              title: Test API
290              version: 1.0.0
291            paths:
292              /items/{item_id}:
293                get:
294                  operationId: getItem
295                  description: Gets an item.
296                  parameters:
297                    - name: item_id
298                      in: path
299                      required: true
300                      schema:
301                        type: string
302                    - name: expand
303                      in: query
304                      schema:
305                        type: boolean
306                  responses:
307                    '200':
308                      description: OK
309        "})
310        .unwrap();
311
312        let arena = Arena::new();
313        let spec = Spec::from_doc(&arena, &doc).unwrap();
314        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
315
316        let op = graph.operations().next().unwrap();
317        let codegen = CodegenOperation::new(&graph, &op);
318
319        let actual: syn::ImplItemFn = parse_quote!(#codegen);
320        let expected: syn::ImplItemFn = parse_quote! {
321            #[doc = " Gets an item."]
322            #[doc = ""]
323            #[doc = " GET /items/{item_id}"]
324            pub async fn get_item(
325                &self,
326                item_id: &str,
327                query: &parameters::GetItemQuery
328            ) -> Result<(), crate::error::Error> {
329                let url = {
330                    let mut url = self.base_url.clone();
331                    let _ = url
332                        .path_segments_mut()
333                        .map(|mut segments| {
334                            segments.pop_if_empty()
335                                .push("items")
336                                .push(item_id);
337                        });
338                    url
339                };
340                let url = ::ploidy_util::serde::Serialize::serialize(
341                    query,
342                    ::ploidy_util::QuerySerializer::new(
343                        url,
344                        parameters::GetItemQuery::STYLES,
345                    ),
346                )?;
347                let response = self
348                    .client
349                    .get(url)
350                    .headers(self.headers.clone())
351                    .send()
352                    .await?
353                    .error_for_status()?;
354                let _ = response;
355                Ok(())
356            }
357        };
358        assert_eq!(actual, expected);
359    }
360
361    #[test]
362    fn test_operation_with_query_params_only() {
363        let doc = Document::from_yaml(indoc::indoc! {"
364            openapi: 3.0.0
365            info:
366              title: Test API
367              version: 1.0.0
368            paths:
369              /items:
370                get:
371                  operationId: getItems
372                  parameters:
373                    - name: limit
374                      in: query
375                      schema:
376                        type: integer
377                        format: int32
378                  responses:
379                    '200':
380                      description: OK
381        "})
382        .unwrap();
383
384        let arena = Arena::new();
385        let spec = Spec::from_doc(&arena, &doc).unwrap();
386        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
387
388        let op = graph.operations().next().unwrap();
389        let codegen = CodegenOperation::new(&graph, &op);
390
391        let actual: syn::ImplItemFn = parse_quote!(#codegen);
392        let expected: syn::ImplItemFn = parse_quote! {
393            #[doc = " GET /items"]
394            pub async fn get_items(
395                &self,
396                query: &parameters::GetItemsQuery
397            ) -> Result<(), crate::error::Error> {
398                let url = {
399                    let mut url = self.base_url.clone();
400                    let _ = url
401                        .path_segments_mut()
402                        .map(|mut segments| {
403                            segments.pop_if_empty()
404                                .push("items");
405                        });
406                    url
407                };
408                let url = ::ploidy_util::serde::Serialize::serialize(
409                    query,
410                    ::ploidy_util::QuerySerializer::new(
411                        url,
412                        parameters::GetItemsQuery::STYLES,
413                    ),
414                )?;
415                let response = self
416                    .client
417                    .get(url)
418                    .headers(self.headers.clone())
419                    .send()
420                    .await?
421                    .error_for_status()?;
422                let _ = response;
423                Ok(())
424            }
425        };
426        assert_eq!(actual, expected);
427    }
428
429    #[test]
430    fn test_path_param_named_query_does_not_shadow() {
431        let doc = Document::from_yaml(indoc::indoc! {"
432            openapi: 3.0.0
433            info:
434              title: Test API
435              version: 1.0.0
436            paths:
437              /search/{query}:
438                get:
439                  operationId: search
440                  parameters:
441                    - name: query
442                      in: path
443                      required: true
444                      schema:
445                        type: string
446                    - name: limit
447                      in: query
448                      schema:
449                        type: integer
450                        format: int32
451                  responses:
452                    '200':
453                      description: OK
454        "})
455        .unwrap();
456
457        let arena = Arena::new();
458        let spec = Spec::from_doc(&arena, &doc).unwrap();
459        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
460
461        let op = graph.operations().next().unwrap();
462        let codegen = CodegenOperation::new(&graph, &op);
463
464        let actual: syn::ImplItemFn = parse_quote!(#codegen);
465        let expected: syn::ImplItemFn = parse_quote! {
466            #[doc = " GET /search/{query}"]
467            pub async fn search(
468                &self,
469                query_2: &str,
470                query: &parameters::SearchQuery
471            ) -> Result<(), crate::error::Error> {
472                let url = {
473                    let mut url = self.base_url.clone();
474                    let _ = url
475                        .path_segments_mut()
476                        .map(|mut segments| {
477                            segments.pop_if_empty()
478                                .push("search")
479                                .push(query_2);
480                        });
481                    url
482                };
483                let url = ::ploidy_util::serde::Serialize::serialize(
484                    query,
485                    ::ploidy_util::QuerySerializer::new(
486                        url,
487                        parameters::SearchQuery::STYLES,
488                    ),
489                )?;
490                let response = self
491                    .client
492                    .get(url)
493                    .headers(self.headers.clone())
494                    .send()
495                    .await?
496                    .error_for_status()?;
497                let _ = response;
498                Ok(())
499            }
500        };
501        assert_eq!(actual, expected);
502    }
503
504    // MARK: With query params and request body
505
506    #[test]
507    fn test_operation_with_query_params_and_request_body() {
508        let doc = Document::from_yaml(indoc::indoc! {"
509            openapi: 3.0.0
510            info:
511              title: Test API
512              version: 1.0.0
513            paths:
514              /items/{item_id}:
515                put:
516                  operationId: updateItem
517                  parameters:
518                    - name: item_id
519                      in: path
520                      required: true
521                      schema:
522                        type: string
523                    - name: dry_run
524                      in: query
525                      schema:
526                        type: boolean
527                  requestBody:
528                    content:
529                      application/json:
530                        schema:
531                          $ref: '#/components/schemas/Item'
532                  responses:
533                    '200':
534                      description: OK
535                      content:
536                        application/json:
537                          schema:
538                            $ref: '#/components/schemas/Item'
539            components:
540              schemas:
541                Item:
542                  type: object
543                  properties:
544                    name:
545                      type: string
546        "})
547        .unwrap();
548
549        let arena = Arena::new();
550        let spec = Spec::from_doc(&arena, &doc).unwrap();
551        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
552
553        let op = graph.operations().next().unwrap();
554        let codegen = CodegenOperation::new(&graph, &op);
555
556        let actual: syn::ImplItemFn = parse_quote!(#codegen);
557        let expected: syn::ImplItemFn = parse_quote! {
558            #[doc = " PUT /items/{item_id}"]
559            pub async fn update_item(
560                &self,
561                item_id: &str,
562                query: &parameters::UpdateItemQuery,
563                request: impl Into<crate::types::Item>
564            ) -> Result<crate::types::Item, crate::error::Error> {
565                let url = {
566                    let mut url = self.base_url.clone();
567                    let _ = url
568                        .path_segments_mut()
569                        .map(|mut segments| {
570                            segments.pop_if_empty()
571                                .push("items")
572                                .push(item_id);
573                        });
574                    url
575                };
576                let url = ::ploidy_util::serde::Serialize::serialize(
577                    query,
578                    ::ploidy_util::QuerySerializer::new(
579                        url,
580                        parameters::UpdateItemQuery::STYLES,
581                    ),
582                )?;
583                let response = self
584                    .client
585                    .put(url)
586                    .headers(self.headers.clone())
587                    .json(&request.into())
588                    .send()
589                    .await?
590                    .error_for_status()?;
591                let body = response.bytes().await?;
592                let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
593                let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)
594                    .map_err(crate::error::JsonError::from)?;
595                Ok(result)
596            }
597        };
598        assert_eq!(actual, expected);
599    }
600
601    // MARK: Without query params
602
603    #[test]
604    fn test_operation_without_query_params() {
605        let doc = Document::from_yaml(indoc::indoc! {"
606            openapi: 3.0.0
607            info:
608              title: Test API
609              version: 1.0.0
610            paths:
611              /items/{item_id}:
612                get:
613                  operationId: getItem
614                  parameters:
615                    - name: item_id
616                      in: path
617                      required: true
618                      schema:
619                        type: string
620                  responses:
621                    '200':
622                      description: OK
623        "})
624        .unwrap();
625
626        let arena = Arena::new();
627        let spec = Spec::from_doc(&arena, &doc).unwrap();
628        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
629
630        let op = graph.operations().next().unwrap();
631        let codegen = CodegenOperation::new(&graph, &op);
632
633        let actual: syn::ImplItemFn = parse_quote!(#codegen);
634        let expected: syn::ImplItemFn = parse_quote! {
635            #[doc = " GET /items/{item_id}"]
636            pub async fn get_item(
637                &self,
638                item_id: &str
639            ) -> Result<(), crate::error::Error> {
640                let url = {
641                    let mut url = self.base_url.clone();
642                    let _ = url
643                        .path_segments_mut()
644                        .map(|mut segments| {
645                            segments.pop_if_empty()
646                                .push("items")
647                                .push(item_id);
648                        });
649                    url
650                };
651                let response = self
652                    .client
653                    .get(url)
654                    .headers(self.headers.clone())
655                    .send()
656                    .await?
657                    .error_for_status()?;
658                let _ = response;
659                Ok(())
660            }
661        };
662        assert_eq!(actual, expected);
663    }
664
665    // MARK: Synthesized path params
666
667    #[test]
668    fn test_operation_with_synthesized_path_param() {
669        let doc = Document::from_yaml(indoc::indoc! {"
670            openapi: 3.0.0
671            info:
672              title: Test API
673              version: 1.0.0
674            paths:
675              /items/{item_id}:
676                get:
677                  operationId: getItem
678                  responses:
679                    '200':
680                      description: OK
681        "})
682        .unwrap();
683
684        let arena = Arena::new();
685        let spec = Spec::from_doc(&arena, &doc).unwrap();
686        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
687
688        let op = graph.operations().next().unwrap();
689        let codegen = CodegenOperation::new(&graph, &op);
690
691        let actual: syn::ImplItemFn = parse_quote!(#codegen);
692        let expected: syn::ImplItemFn = parse_quote! {
693            #[doc = " GET /items/{item_id}"]
694            pub async fn get_item(
695                &self,
696                item_id: &str
697            ) -> Result<(), crate::error::Error> {
698                let url = {
699                    let mut url = self.base_url.clone();
700                    let _ = url
701                        .path_segments_mut()
702                        .map(|mut segments| {
703                            segments.pop_if_empty()
704                                .push("items")
705                                .push(item_id);
706                        });
707                    url
708                };
709                let response = self
710                    .client
711                    .get(url)
712                    .headers(self.headers.clone())
713                    .send()
714                    .await?
715                    .error_for_status()?;
716                let _ = response;
717                Ok(())
718            }
719        };
720        assert_eq!(actual, expected);
721    }
722
723    // MARK: Literal query params in path
724
725    #[test]
726    fn test_operation_with_literal_query_params() {
727        let doc = Document::from_yaml(indoc::indoc! {"
728            openapi: 3.0.0
729            info:
730              title: Test API
731              version: 1.0.0
732            paths:
733              /v1/messages?beta=true&expand:
734                post:
735                  operationId: betaCreateMessage
736                  requestBody:
737                    content:
738                      application/json:
739                        schema:
740                          $ref: '#/components/schemas/Message'
741                  responses:
742                    '200':
743                      description: OK
744                      content:
745                        application/json:
746                          schema:
747                            $ref: '#/components/schemas/Message'
748            components:
749              schemas:
750                Message:
751                  type: object
752                  properties:
753                    content:
754                      type: string
755        "})
756        .unwrap();
757
758        let arena = Arena::new();
759        let spec = Spec::from_doc(&arena, &doc).unwrap();
760        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
761
762        let op = graph.operations().next().unwrap();
763        let codegen = CodegenOperation::new(&graph, &op);
764
765        let actual: syn::ImplItemFn = parse_quote!(#codegen);
766        let expected: syn::ImplItemFn = parse_quote! {
767            #[doc = " POST /v1/messages?beta=true&expand="]
768            pub async fn beta_create_message(
769                &self,
770                request: impl Into<crate::types::Message>
771            ) -> Result<crate::types::Message, crate::error::Error> {
772                let url = {
773                    let mut url = self.base_url.clone();
774                    let _ = url
775                        .path_segments_mut()
776                        .map(|mut segments| {
777                            segments.pop_if_empty()
778                                .extend(&["v1", "messages"]);
779                        });
780                    url.query_pairs_mut()
781                        .append_pair("beta", "true")
782                        .append_pair("expand", "");
783                    url
784                };
785                let response = self
786                    .client
787                    .post(url)
788                    .headers(self.headers.clone())
789                    .json(&request.into())
790                    .send()
791                    .await?
792                    .error_for_status()?;
793                let body = response.bytes().await?;
794                let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
795                let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)
796                    .map_err(crate::error::JsonError::from)?;
797                Ok(result)
798            }
799        };
800        assert_eq!(actual, expected);
801    }
802
803    #[test]
804    fn test_operation_with_literal_and_declared_query_params() {
805        let doc = Document::from_yaml(indoc::indoc! {"
806            openapi: 3.0.0
807            info:
808              title: Test API
809              version: 1.0.0
810            paths:
811              /v1/messages?beta=true:
812                post:
813                  operationId: betaCreateMessage
814                  parameters:
815                    - name: limit
816                      in: query
817                      schema:
818                        type: integer
819                        format: int32
820                  requestBody:
821                    content:
822                      application/json:
823                        schema:
824                          $ref: '#/components/schemas/Message'
825                  responses:
826                    '200':
827                      description: OK
828                      content:
829                        application/json:
830                          schema:
831                            $ref: '#/components/schemas/Message'
832            components:
833              schemas:
834                Message:
835                  type: object
836                  properties:
837                    content:
838                      type: string
839        "})
840        .unwrap();
841
842        let arena = Arena::new();
843        let spec = Spec::from_doc(&arena, &doc).unwrap();
844        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
845
846        let op = graph.operations().next().unwrap();
847        let codegen = CodegenOperation::new(&graph, &op);
848
849        let actual: syn::ImplItemFn = parse_quote!(#codegen);
850        let expected: syn::ImplItemFn = parse_quote! {
851            #[doc = " POST /v1/messages?beta=true"]
852            pub async fn beta_create_message(
853                &self,
854                query: &parameters::BetaCreateMessageQuery,
855                request: impl Into<crate::types::Message>
856            ) -> Result<crate::types::Message, crate::error::Error> {
857                let url = {
858                    let mut url = self.base_url.clone();
859                    let _ = url
860                        .path_segments_mut()
861                        .map(|mut segments| {
862                            segments.pop_if_empty()
863                                .extend(&["v1", "messages"]);
864                        });
865                    url.query_pairs_mut()
866                        .append_pair("beta", "true");
867                    url
868                };
869                let url = ::ploidy_util::serde::Serialize::serialize(
870                    query,
871                    ::ploidy_util::QuerySerializer::new(
872                        url,
873                        parameters::BetaCreateMessageQuery::STYLES,
874                    ),
875                )?;
876                let response = self
877                    .client
878                    .post(url)
879                    .headers(self.headers.clone())
880                    .json(&request.into())
881                    .send()
882                    .await?
883                    .error_for_status()?;
884                let body = response.bytes().await?;
885                let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
886                let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)
887                    .map_err(crate::error::JsonError::from)?;
888                Ok(result)
889            }
890        };
891        assert_eq!(actual, expected);
892    }
893
894    #[test]
895    fn test_operation_with_path_params_and_literal_and_declared_query_params() {
896        let doc = Document::from_yaml(indoc::indoc! {"
897            openapi: 3.0.0
898            info:
899              title: Test API
900              version: 1.0.0
901            paths:
902              /v1/models/{model_id}?beta=true:
903                get:
904                  operationId: betaGetModel
905                  parameters:
906                    - name: model_id
907                      in: path
908                      required: true
909                      schema:
910                        type: string
911                    - name: expand
912                      in: query
913                      schema:
914                        type: boolean
915                  responses:
916                    '200':
917                      description: OK
918                      content:
919                        application/json:
920                          schema:
921                            $ref: '#/components/schemas/Model'
922            components:
923              schemas:
924                Model:
925                  type: object
926                  properties:
927                    id:
928                      type: string
929        "})
930        .unwrap();
931
932        let arena = Arena::new();
933        let spec = Spec::from_doc(&arena, &doc).unwrap();
934        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
935
936        let op = graph.operations().next().unwrap();
937        let codegen = CodegenOperation::new(&graph, &op);
938
939        let actual: syn::ImplItemFn = parse_quote!(#codegen);
940        let expected: syn::ImplItemFn = parse_quote! {
941            #[doc = " GET /v1/models/{model_id}?beta=true"]
942            pub async fn beta_get_model(
943                &self,
944                model_id: &str,
945                query: &parameters::BetaGetModelQuery
946            ) -> Result<crate::types::Model, crate::error::Error> {
947                let url = {
948                    let mut url = self.base_url.clone();
949                    let _ = url
950                        .path_segments_mut()
951                        .map(|mut segments| {
952                            segments.pop_if_empty()
953                                .extend(&["v1", "models"])
954                                .push(model_id);
955                        });
956                    url.query_pairs_mut()
957                        .append_pair("beta", "true");
958                    url
959                };
960                let url = ::ploidy_util::serde::Serialize::serialize(
961                    query,
962                    ::ploidy_util::QuerySerializer::new(
963                        url,
964                        parameters::BetaGetModelQuery::STYLES,
965                    ),
966                )?;
967                let response = self
968                    .client
969                    .get(url)
970                    .headers(self.headers.clone())
971                    .send()
972                    .await?
973                    .error_for_status()?;
974                let body = response.bytes().await?;
975                let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
976                let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)
977                    .map_err(crate::error::JsonError::from)?;
978                Ok(result)
979            }
980        };
981        assert_eq!(actual, expected);
982    }
983}