Skip to main content

ploidy_codegen_rust/
operation.rs

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