Skip to main content

ploidy_codegen_rust/
resource.rs

1use ploidy_core::{
2    codegen::IntoCode,
3    ir::{OperationView, View},
4};
5use proc_macro2::TokenStream;
6use quote::{ToTokens, TokenStreamExt, format_ident, quote};
7
8use super::{
9    cfg::CfgFeature,
10    graph::CodegenGraph,
11    inlines::CodegenInlines,
12    naming::{CodegenIdentUsage, ResourceGroup},
13    operation::CodegenOperation,
14    query::CodegenQueryParameters,
15};
16
17/// Generates an `impl Client` block for a feature-gated resource,
18/// with all its operations and inline types.
19pub struct CodegenResource<'a> {
20    graph: &'a CodegenGraph<'a>,
21    resource: ResourceGroup<'a>,
22    ops: &'a [OperationView<'a, 'a>],
23}
24
25impl<'a> CodegenResource<'a> {
26    pub fn new(
27        graph: &'a CodegenGraph<'a>,
28        resource: ResourceGroup<'a>,
29        ops: &'a [OperationView<'a, 'a>],
30    ) -> Self {
31        Self {
32            graph,
33            resource,
34            ops,
35        }
36    }
37}
38
39impl ToTokens for CodegenResource<'_> {
40    #[allow(
41        clippy::filter_map_bool_then,
42        reason = "`filter_map` + `then` reads cleaner here"
43    )]
44    fn to_tokens(&self, tokens: &mut TokenStream) {
45        let methods = self.ops.iter().map(|op| {
46            // Each method gets its own `#[cfg(...)]` attribute.
47            let cfg = CfgFeature::for_operation(self.graph, op);
48            let method = CodegenOperation::new(self.graph, op);
49            quote! {
50                #cfg
51                #method
52            }
53        });
54
55        let inlines = CodegenInlines::for_resource_inlines(
56            self.graph,
57            self.ops.iter().flat_map(|op| op.inlines()).collect(),
58        );
59
60        let params = self
61            .ops
62            .iter()
63            .filter_map(|op| {
64                // Collect query parameter structs for operations
65                // that have at least one query parameter.
66                op.query().next().is_some().then(|| {
67                    let cfg = CfgFeature::for_operation(self.graph, op);
68                    let query = CodegenQueryParameters::new(self.graph, op);
69                    let mod_name = format_ident!(
70                        "{}_query",
71                        CodegenIdentUsage::Module(self.graph.ident(op.id()))
72                    );
73                    quote! {
74                        #cfg
75                        mod #mod_name {
76                            #query
77                        }
78                        #cfg
79                        pub use #mod_name::*;
80                    }
81                })
82            })
83            .reduce(|a, b| quote!(#a #b))
84            .map(|params| {
85                quote! {
86                    pub mod parameters {
87                        #params
88                    }
89                }
90            });
91
92        tokens.append_all(quote! {
93            impl crate::client::Client {
94                #(#methods)*
95            }
96            #params
97            #inlines
98        });
99    }
100}
101
102impl IntoCode for CodegenResource<'_> {
103    type Code = (String, TokenStream);
104
105    fn into_code(self) -> Self::Code {
106        (
107            match self.resource {
108                ResourceGroup::Named(name) => format!(
109                    "src/client/{}.rs",
110                    CodegenIdentUsage::Module(name).display()
111                ),
112                ResourceGroup::Default => "src/client/default.rs".to_owned(),
113            },
114            self.into_token_stream(),
115        )
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    use itertools::Itertools;
124    use ploidy_core::{
125        arena::Arena,
126        ir::{RawGraph, Spec},
127        parse::Document,
128    };
129    use pretty_assertions::assert_eq;
130    use syn::parse_quote;
131
132    use crate::graph::CodegenGraph;
133
134    // MARK: Feature gating
135
136    #[test]
137    fn test_operation_method_with_only_unnamed_deps_has_no_cfg() {
138        let doc = Document::from_yaml(indoc::indoc! {"
139            openapi: 3.0.0
140            info:
141              title: Test
142              version: 1.0.0
143            paths:
144              /customers:
145                get:
146                  operationId: listCustomers
147                  x-resource-name: customer
148                  responses:
149                    '200':
150                      description: OK
151                      content:
152                        application/json:
153                          schema:
154                            type: array
155                            items:
156                              $ref: '#/components/schemas/Customer'
157            components:
158              schemas:
159                Customer:
160                  type: object
161                  properties:
162                    address:
163                      $ref: '#/components/schemas/Address'
164                Address:
165                  type: object
166                  properties:
167                    street:
168                      type: string
169        "})
170        .unwrap();
171
172        let arena = Arena::new();
173        let spec = Spec::from_doc(&arena, &doc).unwrap();
174        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
175
176        let ops = graph.operations().collect_vec();
177        let [op] = &*ops else {
178            panic!("expected one operation; got `{ops:?}`");
179        };
180        let resource =
181            CodegenResource::new(&graph, graph.resource_for(op), std::slice::from_ref(op));
182
183        // No `#[cfg(...)]` on the method because none of its
184        // dependencies have an `x-resourceId`.
185        let actual: syn::File = parse_quote!(#resource);
186        let expected: syn::File = parse_quote! {
187            impl crate::client::Client {
188                #[doc = " GET /customers"]
189                #[cfg_attr(
190                    feature = "tracing",
191                    ::tracing::instrument(
192                        skip_all,
193                        fields(
194                            otel.name = "GET /customers",
195                            otel.kind = "client",
196                            url.template = "/customers",
197                            http.request.method = "GET",
198                            server.address,
199                            server.port,
200                            url.full,
201                            http.response.status_code,
202                            error.type
203                        )
204                    )
205                )]
206                pub async fn list_customers(
207                    &self,
208                ) -> Result<::std::vec::Vec<crate::types::Customer>, crate::error::Error> {
209                let result: Result<_, crate::error::Error> = async move {
210                    let url = {
211                        let mut url = self.base_url.clone();
212                        url.path_segments_mut()
213                            .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
214                            .pop_if_empty()
215                            .push("customers");
216                        #[cfg(feature = "tracing")]
217                        {
218                            ::tracing::record_all!(::tracing::Span::current(),
219                                server.address = url.host_str(),
220                                server.port = url.port_or_known_default(),
221                                url.full = url.as_str(),
222                            );
223                        }
224                        url
225                    };
226                    let request = {
227                        let request = self
228                            .client
229                            .get(url)
230                            .headers(self.headers.clone());
231                        #[cfg(feature = "trace-context")]
232                        let request = ::ploidy_util::trace::propagate(
233                            ::tracing::Span::current(),
234                            request,
235                        );
236                        request
237                    };
238                    let response = request
239                        .send()
240                        .await?;
241                    #[cfg(feature = "tracing")]
242                    {
243                        ::tracing::record_all!(::tracing::Span::current(),
244                            http.response.status_code = response.status().as_u16()
245                        );
246                    }
247                    let response = response.error_for_status()?;
248                    let body = response.bytes().await?;
249                    let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
250                    let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)?;
251                    Ok(result)
252                }.await;
253                #[cfg(feature = "tracing")]
254                if let Err(err) = &result {
255                    ::tracing::record_all!(::tracing::Span::current(),
256                        error.type = %err.category(),
257                    );
258                }
259                result
260            }
261            }
262        };
263        assert_eq!(actual, expected);
264    }
265
266    #[test]
267    fn test_operation_method_with_named_deps_has_cfg() {
268        let doc = Document::from_yaml(indoc::indoc! {"
269            openapi: 3.0.0
270            info:
271              title: Test
272              version: 1.0.0
273            paths:
274              /orders:
275                get:
276                  operationId: listOrders
277                  x-resource-name: orders
278                  responses:
279                    '200':
280                      description: OK
281                      content:
282                        application/json:
283                          schema:
284                            type: array
285                            items:
286                              $ref: '#/components/schemas/Order'
287            components:
288              schemas:
289                Order:
290                  type: object
291                  properties:
292                    customer:
293                      $ref: '#/components/schemas/Customer'
294                Customer:
295                  type: object
296                  x-resourceId: customer
297                  properties:
298                    id:
299                      type: string
300        "})
301        .unwrap();
302
303        let arena = Arena::new();
304        let spec = Spec::from_doc(&arena, &doc).unwrap();
305        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
306
307        let ops = graph.operations().collect_vec();
308        let [op] = &*ops else {
309            panic!("expected one operation; got `{ops:?}`");
310        };
311        let resource =
312            CodegenResource::new(&graph, graph.resource_for(op), std::slice::from_ref(op));
313
314        // `#[cfg(feature = "customer")]` because `Order` depends on
315        // `Customer`, which has `x-resourceId: customer`.
316        let actual: syn::File = parse_quote!(#resource);
317        let expected: syn::File = parse_quote! {
318            impl crate::client::Client {
319                #[cfg(feature = "customer")]
320                #[doc = " GET /orders"]
321                #[cfg_attr(
322                    feature = "tracing",
323                    ::tracing::instrument(
324                        skip_all,
325                        fields(
326                            otel.name = "GET /orders",
327                            otel.kind = "client",
328                            url.template = "/orders",
329                            http.request.method = "GET",
330                            server.address,
331                            server.port,
332                            url.full,
333                            http.response.status_code,
334                            error.type
335                        )
336                    )
337                )]
338                pub async fn list_orders(
339                    &self,
340                ) -> Result<::std::vec::Vec<crate::types::Order>, crate::error::Error> {
341                let result: Result<_, crate::error::Error> = async move {
342                    let url = {
343                        let mut url = self.base_url.clone();
344                        url.path_segments_mut()
345                            .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
346                            .pop_if_empty()
347                            .push("orders");
348                        #[cfg(feature = "tracing")]
349                        {
350                            ::tracing::record_all!(::tracing::Span::current(),
351                                server.address = url.host_str(),
352                                server.port = url.port_or_known_default(),
353                                url.full = url.as_str(),
354                            );
355                        }
356                        url
357                    };
358                    let request = {
359                        let request = self
360                            .client
361                            .get(url)
362                            .headers(self.headers.clone());
363                        #[cfg(feature = "trace-context")]
364                        let request = ::ploidy_util::trace::propagate(
365                            ::tracing::Span::current(),
366                            request,
367                        );
368                        request
369                    };
370                    let response = request
371                        .send()
372                        .await?;
373                    #[cfg(feature = "tracing")]
374                    {
375                        ::tracing::record_all!(::tracing::Span::current(),
376                            http.response.status_code = response.status().as_u16()
377                        );
378                    }
379                    let response = response.error_for_status()?;
380                    let body = response.bytes().await?;
381                    let deserializer = &mut ::ploidy_util::serde_json::Deserializer::from_slice(&body);
382                    let result = ::ploidy_util::serde_path_to_error::deserialize(deserializer)?;
383                    Ok(result)
384                }.await;
385                #[cfg(feature = "tracing")]
386                if let Err(err) = &result {
387                    ::tracing::record_all!(::tracing::Span::current(),
388                        error.type = %err.category(),
389                    );
390                }
391                result
392            }
393            }
394        };
395        assert_eq!(actual, expected);
396    }
397
398    // MARK: Parameters module
399
400    #[test]
401    fn test_resource_emits_parameters_module() {
402        let doc = Document::from_yaml(indoc::indoc! {"
403            openapi: 3.0.0
404            info:
405              title: Test
406              version: 1.0.0
407            paths:
408              /customers:
409                get:
410                  operationId: listCustomers
411                  x-resource-name: customer
412                  parameters:
413                    - name: limit
414                      in: query
415                      schema:
416                        type: integer
417                        format: int32
418                  responses:
419                    '200':
420                      description: OK
421        "})
422        .unwrap();
423
424        let arena = Arena::new();
425        let spec = Spec::from_doc(&arena, &doc).unwrap();
426        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
427
428        let ops = graph.operations().collect_vec();
429        let [op] = &*ops else {
430            panic!("expected one operation; got `{ops:?}`");
431        };
432        let resource =
433            CodegenResource::new(&graph, graph.resource_for(op), std::slice::from_ref(op));
434
435        let actual: syn::File = parse_quote!(#resource);
436        let expected: syn::File = parse_quote! {
437            impl crate::client::Client {
438                #[doc = " GET /customers"]
439                #[cfg_attr(
440                    feature = "tracing",
441                    ::tracing::instrument(
442                        skip_all,
443                        fields(
444                            otel.name = "GET /customers",
445                            otel.kind = "client",
446                            url.template = "/customers",
447                            http.request.method = "GET",
448                            server.address,
449                            server.port,
450                            url.full,
451                            http.response.status_code,
452                            error.type
453                        )
454                    )
455                )]
456                pub async fn list_customers(
457                    &self,
458                    query: &parameters::ListCustomersQuery
459                ) -> Result<(), crate::error::Error> {
460                let result: Result<_, crate::error::Error> = async move {
461                    let url = {
462                        let mut url = self.base_url.clone();
463                        url.path_segments_mut()
464                            .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
465                            .pop_if_empty()
466                            .push("customers");
467                        let url = ::ploidy_util::serde::Serialize::serialize(
468                            query,
469                            ::ploidy_util::QuerySerializer::new(
470                                url,
471                                parameters::ListCustomersQuery::STYLES,
472                            ),
473                        )?;
474                        #[cfg(feature = "tracing")]
475                        {
476                            ::tracing::record_all!(::tracing::Span::current(),
477                                server.address = url.host_str(),
478                                server.port = url.port_or_known_default(),
479                                url.full = url.as_str(),
480                            );
481                        }
482                        url
483                    };
484                    let request = {
485                        let request = self
486                            .client
487                            .get(url)
488                            .headers(self.headers.clone());
489                        #[cfg(feature = "trace-context")]
490                        let request = ::ploidy_util::trace::propagate(
491                            ::tracing::Span::current(),
492                            request,
493                        );
494                        request
495                    };
496                    let response = request
497                        .send()
498                        .await?;
499                    #[cfg(feature = "tracing")]
500                    {
501                        ::tracing::record_all!(::tracing::Span::current(),
502                            http.response.status_code = response.status().as_u16()
503                        );
504                    }
505                    let response = response.error_for_status()?;
506                    let _ = response;
507                    Ok(())
508                }.await;
509                #[cfg(feature = "tracing")]
510                if let Err(err) = &result {
511                    ::tracing::record_all!(::tracing::Span::current(),
512                        error.type = %err.category(),
513                    );
514                }
515                result
516            }
517            }
518            pub mod parameters {
519                mod list_customers_query {
520                    #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
521                    #[serde(crate = "::ploidy_util::serde")]
522                    pub struct ListCustomersQuery {
523                        #[serde(default, skip_serializing_if = "Option::is_none")]
524                        pub limit: ::std::option::Option<i32>,
525                    }
526                    impl ListCustomersQuery {
527                        pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[];
528                    }
529                }
530                pub use list_customers_query::*;
531            }
532        };
533        assert_eq!(actual, expected);
534    }
535
536    #[test]
537    fn test_resource_with_multiple_query_ops_shares_parameters_module() {
538        let doc = Document::from_yaml(indoc::indoc! {"
539            openapi: 3.0.0
540            info:
541              title: Test
542              version: 1.0.0
543            paths:
544              /customers:
545                get:
546                  operationId: listCustomers
547                  x-resource-name: customer
548                  parameters:
549                    - name: limit
550                      in: query
551                      schema:
552                        type: integer
553                        format: int32
554                  responses:
555                    '200':
556                      description: OK
557              /customers/search:
558                get:
559                  operationId: searchCustomers
560                  x-resource-name: customer
561                  parameters:
562                    - name: email
563                      in: query
564                      required: true
565                      schema:
566                        type: string
567                  responses:
568                    '200':
569                      description: OK
570        "})
571        .unwrap();
572
573        let arena = Arena::new();
574        let spec = Spec::from_doc(&arena, &doc).unwrap();
575        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
576
577        let ops = graph.operations().collect_vec();
578        let resource = ops
579            .iter()
580            .map(|op| graph.resource_for(op))
581            .all_equal_value()
582            .unwrap();
583        let resource = CodegenResource::new(&graph, resource, &ops);
584
585        let actual: syn::File = parse_quote!(#resource);
586        let expected: syn::File = parse_quote! {
587            impl crate::client::Client {
588                #[doc = " GET /customers"]
589                #[cfg_attr(
590                    feature = "tracing",
591                    ::tracing::instrument(
592                        skip_all,
593                        fields(
594                            otel.name = "GET /customers",
595                            otel.kind = "client",
596                            url.template = "/customers",
597                            http.request.method = "GET",
598                            server.address,
599                            server.port,
600                            url.full,
601                            http.response.status_code,
602                            error.type
603                        )
604                    )
605                )]
606                pub async fn list_customers(
607                    &self,
608                    query: &parameters::ListCustomersQuery
609                ) -> Result<(), crate::error::Error> {
610                let result: Result<_, crate::error::Error> = async move {
611                    let url = {
612                        let mut url = self.base_url.clone();
613                        url.path_segments_mut()
614                            .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
615                            .pop_if_empty()
616                            .push("customers");
617                        let url = ::ploidy_util::serde::Serialize::serialize(
618                            query,
619                            ::ploidy_util::QuerySerializer::new(
620                                url,
621                                parameters::ListCustomersQuery::STYLES,
622                            ),
623                        )?;
624                        #[cfg(feature = "tracing")]
625                        {
626                            ::tracing::record_all!(::tracing::Span::current(),
627                                server.address = url.host_str(),
628                                server.port = url.port_or_known_default(),
629                                url.full = url.as_str(),
630                            );
631                        }
632                        url
633                    };
634                    let request = {
635                        let request = self
636                            .client
637                            .get(url)
638                            .headers(self.headers.clone());
639                        #[cfg(feature = "trace-context")]
640                        let request = ::ploidy_util::trace::propagate(
641                            ::tracing::Span::current(),
642                            request,
643                        );
644                        request
645                    };
646                    let response = request
647                        .send()
648                        .await?;
649                    #[cfg(feature = "tracing")]
650                    {
651                        ::tracing::record_all!(::tracing::Span::current(),
652                            http.response.status_code = response.status().as_u16()
653                        );
654                    }
655                    let response = response.error_for_status()?;
656                    let _ = response;
657                    Ok(())
658                }.await;
659                #[cfg(feature = "tracing")]
660                if let Err(err) = &result {
661                    ::tracing::record_all!(::tracing::Span::current(),
662                        error.type = %err.category(),
663                    );
664                }
665                result
666            }
667                #[doc = " GET /customers/search"]
668                #[cfg_attr(
669                    feature = "tracing",
670                    ::tracing::instrument(
671                        skip_all,
672                        fields(
673                            otel.name = "GET /customers/search",
674                            otel.kind = "client",
675                            url.template = "/customers/search",
676                            http.request.method = "GET",
677                            server.address,
678                            server.port,
679                            url.full,
680                            http.response.status_code,
681                            error.type
682                        )
683                    )
684                )]
685                pub async fn search_customers(
686                    &self,
687                    query: &parameters::SearchCustomersQuery
688                ) -> Result<(), crate::error::Error> {
689                let result: Result<_, crate::error::Error> = async move {
690                    let url = {
691                        let mut url = self.base_url.clone();
692                        url.path_segments_mut()
693                            .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
694                            .pop_if_empty()
695                            .extend(&["customers", "search"]);
696                        let url = ::ploidy_util::serde::Serialize::serialize(
697                            query,
698                            ::ploidy_util::QuerySerializer::new(
699                                url,
700                                parameters::SearchCustomersQuery::STYLES,
701                            ),
702                        )?;
703                        #[cfg(feature = "tracing")]
704                        {
705                            ::tracing::record_all!(::tracing::Span::current(),
706                                server.address = url.host_str(),
707                                server.port = url.port_or_known_default(),
708                                url.full = url.as_str(),
709                            );
710                        }
711                        url
712                    };
713                    let request = {
714                        let request = self
715                            .client
716                            .get(url)
717                            .headers(self.headers.clone());
718                        #[cfg(feature = "trace-context")]
719                        let request = ::ploidy_util::trace::propagate(
720                            ::tracing::Span::current(),
721                            request,
722                        );
723                        request
724                    };
725                    let response = request
726                        .send()
727                        .await?;
728                    #[cfg(feature = "tracing")]
729                    {
730                        ::tracing::record_all!(::tracing::Span::current(),
731                            http.response.status_code = response.status().as_u16()
732                        );
733                    }
734                    let response = response.error_for_status()?;
735                    let _ = response;
736                    Ok(())
737                }.await;
738                #[cfg(feature = "tracing")]
739                if let Err(err) = &result {
740                    ::tracing::record_all!(::tracing::Span::current(),
741                        error.type = %err.category(),
742                    );
743                }
744                result
745            }
746            }
747            pub mod parameters {
748                mod list_customers_query {
749                    #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
750                    #[serde(crate = "::ploidy_util::serde")]
751                    pub struct ListCustomersQuery {
752                        #[serde(default, skip_serializing_if = "Option::is_none")]
753                        pub limit: ::std::option::Option<i32>,
754                    }
755                    impl ListCustomersQuery {
756                        pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[];
757                    }
758                }
759                pub use list_customers_query::*;
760                mod search_customers_query {
761                    #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
762                    #[serde(crate = "::ploidy_util::serde")]
763                    pub struct SearchCustomersQuery {
764                        pub email: ::std::string::String,
765                    }
766                    impl SearchCustomersQuery {
767                        pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[];
768                    }
769                }
770                pub use search_customers_query::*;
771            }
772        };
773        assert_eq!(actual, expected);
774    }
775
776    #[test]
777    fn test_resource_omits_parameters_module_when_no_query_params() {
778        let doc = Document::from_yaml(indoc::indoc! {"
779            openapi: 3.0.0
780            info:
781              title: Test
782              version: 1.0.0
783            paths:
784              /customers:
785                get:
786                  operationId: listCustomers
787                  x-resource-name: customer
788                  responses:
789                    '200':
790                      description: OK
791        "})
792        .unwrap();
793
794        let arena = Arena::new();
795        let spec = Spec::from_doc(&arena, &doc).unwrap();
796        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
797
798        let ops = graph.operations().collect_vec();
799        let [op] = &*ops else {
800            panic!("expected one operation; got `{ops:?}`");
801        };
802        let resource =
803            CodegenResource::new(&graph, graph.resource_for(op), std::slice::from_ref(op));
804
805        let actual: syn::File = parse_quote!(#resource);
806        let expected: syn::File = parse_quote! {
807            impl crate::client::Client {
808                #[doc = " GET /customers"]
809                #[cfg_attr(
810                    feature = "tracing",
811                    ::tracing::instrument(
812                        skip_all,
813                        fields(
814                            otel.name = "GET /customers",
815                            otel.kind = "client",
816                            url.template = "/customers",
817                            http.request.method = "GET",
818                            server.address,
819                            server.port,
820                            url.full,
821                            http.response.status_code,
822                            error.type
823                        )
824                    )
825                )]
826                pub async fn list_customers(
827                    &self,
828                ) -> Result<(), crate::error::Error> {
829                let result: Result<_, crate::error::Error> = async move {
830                    let url = {
831                        let mut url = self.base_url.clone();
832                        url.path_segments_mut()
833                            .map_err(|()| ::ploidy_util::url::PathAndQueryError::UrlCannotBeABase)?
834                            .pop_if_empty()
835                            .push("customers");
836                        #[cfg(feature = "tracing")]
837                        {
838                            ::tracing::record_all!(::tracing::Span::current(),
839                                server.address = url.host_str(),
840                                server.port = url.port_or_known_default(),
841                                url.full = url.as_str(),
842                            );
843                        }
844                        url
845                    };
846                    let request = {
847                        let request = self
848                            .client
849                            .get(url)
850                            .headers(self.headers.clone());
851                        #[cfg(feature = "trace-context")]
852                        let request = ::ploidy_util::trace::propagate(
853                            ::tracing::Span::current(),
854                            request,
855                        );
856                        request
857                    };
858                    let response = request
859                        .send()
860                        .await?;
861                    #[cfg(feature = "tracing")]
862                    {
863                        ::tracing::record_all!(::tracing::Span::current(),
864                            http.response.status_code = response.status().as_u16()
865                        );
866                    }
867                    let response = response.error_for_status()?;
868                    let _ = response;
869                    Ok(())
870                }.await;
871                #[cfg(feature = "tracing")]
872                if let Err(err) = &result {
873                    ::tracing::record_all!(::tracing::Span::current(),
874                        error.type = %err.category(),
875                    );
876                }
877                result
878            }
879            }
880        };
881        assert_eq!(actual, expected);
882    }
883}