progenitor_middleware_impl/
method.rs

1// Copyright 2025 Oxide Computer Company
2
3use std::{
4    cmp::Ordering,
5    collections::{BTreeMap, BTreeSet},
6    str::FromStr,
7};
8
9use openapiv3::{Components, Parameter, ReferenceOr, Response, StatusCode};
10use proc_macro2::TokenStream;
11use quote::{format_ident, quote, ToTokens};
12use typify::{TypeId, TypeSpace};
13
14use crate::{
15    template::PathTemplate,
16    util::{items, parameter_map, sanitize, unique_ident_from, Case},
17    Error, Generator, Result, TagStyle,
18};
19use crate::{to_schema::ToSchema, util::ReferenceOrExt};
20
21/// The intermediate representation of an operation that will become a method.
22pub(crate) struct OperationMethod {
23    pub operation_id: String,
24    pub tags: Vec<String>,
25    pub method: HttpMethod,
26    pub path: PathTemplate,
27    pub summary: Option<String>,
28    pub description: Option<String>,
29    pub params: Vec<OperationParameter>,
30    pub responses: Vec<OperationResponse>,
31    pub dropshot_paginated: Option<DropshotPagination>,
32    dropshot_websocket: bool,
33    pub error_enum_name: Option<String>,
34}
35
36/// Represents the error response type for an operation.
37/// Can be a single type (backwards compatible) or multiple types requiring an enum.
38#[derive(Debug, Clone)]
39pub enum ErrorResponseType {
40    /// Single error type - no enum needed (backwards compatible)
41    Single(OperationResponseKind),
42    /// Multiple error types - generates an enum
43    Multiple {
44        enum_name: String,
45        variants: Vec<ErrorVariant>,
46    },
47}
48
49/// Represents a variant in a generated error enum.
50#[derive(Debug, Clone)]
51pub struct ErrorVariant {
52    pub variant_name: String,
53    pub typ: OperationResponseKind,
54    pub status_codes: Vec<OperationResponseStatus>,
55}
56
57pub enum HttpMethod {
58    Get,
59    Put,
60    Post,
61    Delete,
62    Options,
63    Head,
64    Patch,
65    Trace,
66}
67
68impl std::str::FromStr for HttpMethod {
69    type Err = Error;
70
71    fn from_str(s: &str) -> Result<Self> {
72        match s {
73            "get" => Ok(Self::Get),
74            "put" => Ok(Self::Put),
75            "post" => Ok(Self::Post),
76            "delete" => Ok(Self::Delete),
77            "options" => Ok(Self::Options),
78            "head" => Ok(Self::Head),
79            "patch" => Ok(Self::Patch),
80            "trace" => Ok(Self::Trace),
81            _ => Err(Error::InternalError(format!("bad method: {}", s))),
82        }
83    }
84}
85impl HttpMethod {
86    fn as_str(&self) -> &'static str {
87        match self {
88            HttpMethod::Get => "get",
89            HttpMethod::Put => "put",
90            HttpMethod::Post => "post",
91            HttpMethod::Delete => "delete",
92            HttpMethod::Options => "options",
93            HttpMethod::Head => "head",
94            HttpMethod::Patch => "patch",
95            HttpMethod::Trace => "trace",
96        }
97    }
98}
99
100struct MethodSigBody {
101    success: TokenStream,
102    error: TokenStream,
103    body: TokenStream,
104}
105
106struct BuilderImpl {
107    doc: String,
108    sig: TokenStream,
109    body: TokenStream,
110}
111
112pub struct DropshotPagination {
113    pub item: TypeId,
114    pub first_page_params: Vec<String>,
115}
116
117pub struct OperationParameter {
118    /// Sanitized parameter name.
119    pub name: String,
120    /// Original parameter name provided by the API.
121    pub api_name: String,
122    pub description: Option<String>,
123    pub typ: OperationParameterType,
124    pub kind: OperationParameterKind,
125}
126
127#[derive(Eq, PartialEq)]
128pub enum OperationParameterType {
129    Type(TypeId),
130    RawBody,
131}
132
133#[derive(Debug, PartialEq, Eq)]
134pub enum OperationParameterKind {
135    Path,
136    Query(bool),
137    Header(bool),
138    // TODO bodies may be optional
139    Body(BodyContentType),
140}
141
142impl OperationParameterKind {
143    fn is_required(&self) -> bool {
144        match self {
145            OperationParameterKind::Path => true,
146            OperationParameterKind::Query(required) => *required,
147            OperationParameterKind::Header(required) => *required,
148            // TODO may be optional
149            OperationParameterKind::Body(_) => true,
150        }
151    }
152    fn is_optional(&self) -> bool {
153        !self.is_required()
154    }
155}
156
157#[derive(Debug, PartialEq, Eq)]
158pub enum BodyContentType {
159    OctetStream,
160    Json,
161    FormUrlencoded,
162    Text(String),
163}
164
165impl FromStr for BodyContentType {
166    type Err = Error;
167
168    fn from_str(s: &str) -> Result<Self> {
169        let offset = s.find(';').unwrap_or(s.len());
170        match &s[..offset] {
171            "application/octet-stream" => Ok(Self::OctetStream),
172            "application/json" => Ok(Self::Json),
173            "application/x-www-form-urlencoded" => Ok(Self::FormUrlencoded),
174            "text/plain" | "text/x-markdown" => Ok(Self::Text(String::from(&s[..offset]))),
175            _ => Err(Error::UnexpectedFormat(format!(
176                "unexpected content type: {}",
177                s
178            ))),
179        }
180    }
181}
182
183impl std::fmt::Display for BodyContentType {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        f.write_str(match self {
186            Self::OctetStream => "application/octet-stream",
187            Self::Json => "application/json",
188            Self::FormUrlencoded => "application/x-www-form-urlencoded",
189            Self::Text(typ) => typ,
190        })
191    }
192}
193
194#[derive(Debug)]
195pub(crate) struct OperationResponse {
196    pub status_code: OperationResponseStatus,
197    pub typ: OperationResponseKind,
198    // TODO this isn't currently used because dropshot doesn't give us a
199    // particularly useful message here.
200    #[allow(dead_code)]
201    description: Option<String>,
202}
203
204impl Eq for OperationResponse {}
205impl PartialEq for OperationResponse {
206    fn eq(&self, other: &Self) -> bool {
207        self.status_code == other.status_code
208    }
209}
210impl Ord for OperationResponse {
211    fn cmp(&self, other: &Self) -> Ordering {
212        self.status_code.cmp(&other.status_code)
213    }
214}
215impl PartialOrd for OperationResponse {
216    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
217        Some(self.cmp(other))
218    }
219}
220
221#[derive(Debug, Clone, Eq, PartialEq)]
222pub(crate) enum OperationResponseStatus {
223    Code(u16),
224    Range(u16),
225    Default,
226}
227
228impl OperationResponseStatus {
229    fn to_value(&self) -> u16 {
230        match self {
231            OperationResponseStatus::Code(code) => {
232                assert!(*code < 1000);
233                *code
234            }
235            OperationResponseStatus::Range(range) => {
236                assert!(*range < 10);
237                *range * 100
238            }
239            OperationResponseStatus::Default => 1000,
240        }
241    }
242
243    pub fn is_success_or_default(&self) -> bool {
244        matches!(
245            self,
246            OperationResponseStatus::Default
247                | OperationResponseStatus::Code(101)
248                | OperationResponseStatus::Code(200..=299)
249                | OperationResponseStatus::Range(2)
250        )
251    }
252
253    pub fn is_error_or_default(&self) -> bool {
254        matches!(
255            self,
256            OperationResponseStatus::Default
257                | OperationResponseStatus::Code(400..=599)
258                | OperationResponseStatus::Range(4..=5)
259        )
260    }
261
262    pub fn is_default(&self) -> bool {
263        matches!(self, OperationResponseStatus::Default)
264    }
265}
266
267impl Ord for OperationResponseStatus {
268    fn cmp(&self, other: &Self) -> Ordering {
269        self.to_value().cmp(&other.to_value())
270    }
271}
272
273impl PartialOrd for OperationResponseStatus {
274    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
275        Some(self.cmp(other))
276    }
277}
278
279#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
280pub(crate) enum OperationResponseKind {
281    Type(TypeId),
282    None,
283    Raw,
284    Upgrade,
285}
286
287impl OperationResponseKind {
288    pub fn into_tokens(self, type_space: &TypeSpace) -> TokenStream {
289        match self {
290            OperationResponseKind::Type(ref type_id) => {
291                let type_name = type_space.get_type(type_id).unwrap().ident();
292                quote! { #type_name }
293            }
294            OperationResponseKind::None => {
295                quote! { () }
296            }
297            OperationResponseKind::Raw => {
298                quote! { ByteStream }
299            }
300            OperationResponseKind::Upgrade => {
301                quote! { reqwest::Upgraded }
302            }
303        }
304    }
305}
306
307impl Generator {
308    pub(crate) fn process_operation(
309        &mut self,
310        operation: &openapiv3::Operation,
311        components: &Option<Components>,
312        path: &str,
313        method: &str,
314        path_parameters: &[ReferenceOr<Parameter>],
315    ) -> Result<OperationMethod> {
316        let operation_id = operation.operation_id.as_ref().unwrap();
317
318        let mut combined_path_parameters = parameter_map(path_parameters, components)?;
319        for operation_param in items(&operation.parameters, components) {
320            let parameter = operation_param?;
321            combined_path_parameters.insert(&parameter.parameter_data_ref().name, parameter);
322        }
323
324        // Filter out any path parameters that have been overridden by an
325        // operation parameter
326        let mut params = combined_path_parameters
327            .values()
328            .map(|parameter| {
329                match parameter {
330                    openapiv3::Parameter::Path {
331                        parameter_data,
332                        style: openapiv3::PathStyle::Simple,
333                    } => {
334                        // Path parameters MUST be required.
335                        assert!(parameter_data.required);
336
337                        let schema = parameter_data.schema()?.to_schema();
338
339                        let name = sanitize(
340                            &format!("{}-{}", operation_id, &parameter_data.name),
341                            Case::Pascal,
342                        );
343                        let typ = self.type_space.add_type_with_name(&schema, Some(name))?;
344
345                        Ok(OperationParameter {
346                            name: sanitize(&parameter_data.name, Case::Snake),
347                            api_name: parameter_data.name.clone(),
348                            description: parameter_data.description.clone(),
349                            typ: OperationParameterType::Type(typ),
350                            kind: OperationParameterKind::Path,
351                        })
352                    }
353                    openapiv3::Parameter::Query {
354                        parameter_data,
355                        allow_reserved: _, // We always encode reserved chars
356                        style: openapiv3::QueryStyle::Form,
357                        allow_empty_value: _, // Irrelevant for this client
358                    } => {
359                        let schema = parameter_data.schema()?.to_schema();
360                        let name = sanitize(
361                            &format!(
362                                "{}-{}",
363                                operation.operation_id.as_ref().unwrap(),
364                                &parameter_data.name,
365                            ),
366                            Case::Pascal,
367                        );
368
369                        let type_id = self.type_space.add_type_with_name(&schema, Some(name))?;
370
371                        let ty = self.type_space.get_type(&type_id).unwrap();
372
373                        // If the type is itself optional, then we'll treat it
374                        // as optional (irrespective of the `required` field on
375                        // the parameter) and use the "inner" type.
376                        let details = ty.details();
377                        let (type_id, required) =
378                            if let typify::TypeDetails::Option(inner_type_id) = details {
379                                (inner_type_id, false)
380                            } else {
381                                (type_id, parameter_data.required)
382                            };
383
384                        Ok(OperationParameter {
385                            name: sanitize(&parameter_data.name, Case::Snake),
386                            api_name: parameter_data.name.clone(),
387                            description: parameter_data.description.clone(),
388                            typ: OperationParameterType::Type(type_id),
389                            kind: OperationParameterKind::Query(required),
390                        })
391                    }
392                    openapiv3::Parameter::Header {
393                        parameter_data,
394                        style: openapiv3::HeaderStyle::Simple,
395                    } => {
396                        let schema = parameter_data.schema()?.to_schema();
397                        let name = sanitize(
398                            &format!(
399                                "{}-{}",
400                                operation.operation_id.as_ref().unwrap(),
401                                &parameter_data.name,
402                            ),
403                            Case::Pascal,
404                        );
405
406                        let typ = self.type_space.add_type_with_name(&schema, Some(name))?;
407
408                        Ok(OperationParameter {
409                            name: sanitize(&parameter_data.name, Case::Snake),
410                            api_name: parameter_data.name.clone(),
411                            description: parameter_data.description.clone(),
412                            typ: OperationParameterType::Type(typ),
413                            kind: OperationParameterKind::Header(parameter_data.required),
414                        })
415                    }
416                    openapiv3::Parameter::Path { style, .. } => Err(Error::UnexpectedFormat(
417                        format!("unsupported style of path parameter {:#?}", style,),
418                    )),
419                    openapiv3::Parameter::Query { style, .. } => Err(Error::UnexpectedFormat(
420                        format!("unsupported style of query parameter {:#?}", style,),
421                    )),
422                    cookie @ openapiv3::Parameter::Cookie { .. } => Err(Error::UnexpectedFormat(
423                        format!("cookie parameters are not supported {:#?}", cookie,),
424                    )),
425                }
426            })
427            .collect::<Result<Vec<_>>>()?;
428
429        let dropshot_websocket = operation.extensions.get("x-dropshot-websocket").is_some();
430        if dropshot_websocket {
431            self.uses_websockets = true;
432        }
433
434        if let Some(body_param) = self.get_body_param(operation, components)? {
435            params.push(body_param);
436        }
437
438        let tmp = crate::template::parse(path)?;
439        let names = tmp.names();
440
441        sort_params(&mut params, &names);
442
443        let mut success = false;
444
445        let mut responses =
446            operation
447                .responses
448                .default
449                .iter()
450                .map(|response_or_ref| {
451                    Ok((
452                        OperationResponseStatus::Default,
453                        response_or_ref.item(components)?,
454                    ))
455                })
456                .chain(operation.responses.responses.iter().map(
457                    |(status_code, response_or_ref)| {
458                        Ok((
459                            match status_code {
460                                StatusCode::Code(code) => OperationResponseStatus::Code(*code),
461                                StatusCode::Range(range) => OperationResponseStatus::Range(*range),
462                            },
463                            response_or_ref.item(components)?,
464                        ))
465                    },
466                ))
467                .map(|v: Result<(OperationResponseStatus, &Response)>| {
468                    let (status_code, response) = v?;
469
470                    // We categorize responses as "typed" based on the
471                    // "application/json" content type, "upgrade" if it's a
472                    // websocket channel without a meaningful content-type,
473                    // "raw" if there's any other response content type (we don't
474                    // investigate further), or "none" if there is no content.
475                    // TODO if there are multiple response content types we could
476                    // treat those like different response types and create an
477                    // enum; the generated client method would check for the
478                    // content type of the response just as it currently examines
479                    // the status code.
480                    let typ = if let Some(mt) = response.content.iter().find_map(|(x, v)| {
481                        (x == "application/json" || x.starts_with("application/json;")).then_some(v)
482                    }) {
483                        assert!(mt.encoding.is_empty());
484
485                        let typ = if let Some(schema) = &mt.schema {
486                            let schema = schema.to_schema();
487                            let name = sanitize(
488                                &format!("{}-response", operation.operation_id.as_ref().unwrap(),),
489                                Case::Pascal,
490                            );
491                            self.type_space.add_type_with_name(&schema, Some(name))?
492                        } else {
493                            todo!("media type encoding, no schema: {:#?}", mt);
494                        };
495
496                        OperationResponseKind::Type(typ)
497                    } else if dropshot_websocket {
498                        OperationResponseKind::Upgrade
499                    } else if response.content.first().is_some() {
500                        OperationResponseKind::Raw
501                    } else {
502                        OperationResponseKind::None
503                    };
504
505                    // See if there's a status code that covers success cases.
506                    if matches!(
507                        status_code,
508                        OperationResponseStatus::Default
509                            | OperationResponseStatus::Code(200..=299)
510                            | OperationResponseStatus::Range(2)
511                    ) {
512                        success = true;
513                    }
514
515                    let description = if response.description.is_empty() {
516                        None
517                    } else {
518                        Some(response.description.clone())
519                    };
520
521                    Ok(OperationResponse {
522                        status_code,
523                        typ,
524                        description,
525                    })
526                })
527                .collect::<Result<Vec<_>>>()?;
528
529        // If the API has declined to specify the characteristics of a
530        // successful response, we cons up a generic one. Note that this is
531        // technically permissible within OpenAPI, but advised against by the
532        // spec.
533        if !success {
534            responses.push(OperationResponse {
535                status_code: OperationResponseStatus::Range(2),
536                typ: OperationResponseKind::Raw,
537                description: None,
538            });
539        }
540
541        // Must accept HTTP 101 Switching Protocols
542        if dropshot_websocket {
543            responses.push(OperationResponse {
544                status_code: OperationResponseStatus::Code(101),
545                typ: OperationResponseKind::Upgrade,
546                description: None,
547            })
548        }
549
550        let dropshot_paginated = self.dropshot_pagination_data(operation, &params, &responses);
551
552        if dropshot_websocket && dropshot_paginated.is_some() {
553            return Err(Error::InvalidExtension(format!(
554                "conflicting extensions in {:?}",
555                operation_id
556            )));
557        }
558
559        Ok(OperationMethod {
560            operation_id: sanitize(operation_id, Case::Snake),
561            tags: operation.tags.clone(),
562            method: HttpMethod::from_str(method)?,
563            path: tmp,
564            summary: operation.summary.clone().filter(|s| !s.is_empty()),
565            description: operation.description.clone().filter(|s| !s.is_empty()),
566            params,
567            responses,
568            dropshot_paginated,
569            dropshot_websocket,
570            error_enum_name: None, // Will be set during error response extraction
571        })
572    }
573
574    pub(crate) fn positional_method(
575        &mut self,
576        method: &OperationMethod,
577        has_inner: bool,
578    ) -> Result<TokenStream> {
579        let operation_id = format_ident!("{}", method.operation_id);
580
581        // Render each parameter as it will appear in the method signature.
582        let params = method
583            .params
584            .iter()
585            .map(|param| {
586                let name = format_ident!("{}", param.name);
587                let typ = match (&param.typ, param.kind.is_optional()) {
588                    (OperationParameterType::Type(type_id), false) => self
589                        .type_space
590                        .get_type(type_id)
591                        .unwrap()
592                        .parameter_ident_with_lifetime("a"),
593                    (OperationParameterType::Type(type_id), true) => {
594                        let t = self
595                            .type_space
596                            .get_type(type_id)
597                            .unwrap()
598                            .parameter_ident_with_lifetime("a");
599                        quote! { Option<#t> }
600                    }
601                    (OperationParameterType::RawBody, false) => match &param.kind {
602                        OperationParameterKind::Body(BodyContentType::OctetStream) => {
603                            quote! { B }
604                        }
605                        OperationParameterKind::Body(BodyContentType::Text(_)) => {
606                            quote! { String }
607                        }
608                        _ => unreachable!(),
609                    },
610                    (OperationParameterType::RawBody, true) => unreachable!(),
611                };
612                quote! {
613                    #name: #typ
614                }
615            })
616            .collect::<Vec<_>>();
617
618        let raw_body_param = method.params.iter().any(|param| {
619            param.typ == OperationParameterType::RawBody
620                && param.kind == OperationParameterKind::Body(BodyContentType::OctetStream)
621        });
622
623        let bounds = if raw_body_param {
624            quote! { <'a, B: Into<reqwest::Body> > }
625        } else {
626            quote! { <'a> }
627        };
628
629        let doc_comment = make_doc_comment(method);
630
631        let MethodSigBody {
632            success: success_type,
633            error: error_type,
634            body,
635        } = self.method_sig_body(method, quote! { Self }, quote! { self }, has_inner)?;
636
637        let method_impl = quote! {
638            #[doc = #doc_comment]
639            pub async fn #operation_id #bounds (
640                &'a self,
641                #(#params),*
642            ) -> Result<
643                ResponseValue<#success_type>,
644                Error<#error_type>,
645            > {
646                #body
647            }
648        };
649
650        let stream_impl = method.dropshot_paginated.as_ref().map(|page_data| {
651            // We're now using futures.
652            self.uses_futures = true;
653
654            let stream_id = format_ident!("{}_stream", method.operation_id);
655
656            // The parameters are the same as those to the paged method, but
657            // without "page_token"
658            let stream_params = method
659                .params
660                .iter()
661                .zip(params)
662                .filter_map(|(param, stream)| {
663                    if param.name.as_str() == "page_token" {
664                        None
665                    } else {
666                        Some(stream)
667                    }
668                });
669
670            // The values passed to get the first page are the inputs to the
671            // stream method with "None" for the page_token.
672            let first_params = method.params.iter().map(|param| {
673                if param.api_name.as_str() == "page_token" {
674                    // The page_token is None when getting the first page.
675                    quote! { None }
676                } else {
677                    // All other parameters are passed through directly.
678                    format_ident!("{}", param.name).to_token_stream()
679                }
680            });
681
682            // The values passed to get subsequent pages are...
683            // - the state variable for the page_token
684            // - None for all other query parameters
685            // - The initial inputs for non-query parameters
686            let step_params = method.params.iter().map(|param| {
687                if param.api_name.as_str() == "page_token" {
688                    quote! { state.as_deref() }
689                } else if param.api_name.as_str() != "limit"
690                    && matches!(param.kind, OperationParameterKind::Query(_))
691                {
692                    // Query parameters (other than "page_token" and "limit")
693                    // are None; having page_token as Some(_) is mutually
694                    // exclusive with other query parameters.
695                    quote! { None }
696                } else {
697                    // Non-query parameters are passed in; this is necessary
698                    // e.g. to specify the right path. (We don't really expect
699                    // to see a body parameter here, but we pass it through
700                    // regardless.)
701                    format_ident!("{}", param.name).to_token_stream()
702                }
703            });
704
705            // The item type that we've saved (by picking apart the original
706            // function's return type) will be the Item type parameter for the
707            // Stream type we return.
708            let item = self.type_space.get_type(&page_data.item).unwrap();
709            let item_type = item.ident();
710
711            let doc_comment = make_stream_doc_comment(method);
712
713            quote! {
714                #[doc = #doc_comment]
715                pub fn #stream_id #bounds (
716                    &'a self,
717                    #(#stream_params),*
718                ) -> impl futures::Stream<Item = Result<
719                    #item_type,
720                    Error<#error_type>,
721                >> + Unpin + 'a {
722                    use futures::StreamExt;
723                    use futures::TryFutureExt;
724                    use futures::TryStreamExt;
725
726                    // Execute the operation with the basic parameters
727                    // (omitting page_token) to get the first page.
728                    self.#operation_id( #(#first_params,)* )
729                        .map_ok(move |page| {
730                            let page = page.into_inner();
731
732                            // Create a stream from the items of the first page.
733                            let first =
734                                futures::stream::iter(page.items).map(Ok);
735
736                            // We unfold subsequent pages using page.next_page
737                            // as the seed value. Each iteration returns its
738                            // items and the next page token.
739                            let rest = futures::stream::try_unfold(
740                                page.next_page,
741                                move |state| async move {
742                                    if state.is_none() {
743                                        // The page_token was None so we've
744                                        // reached the end.
745                                        Ok(None)
746                                    } else {
747                                        // Get the next page; here we set all
748                                        // query parameters to None (except for
749                                        // the page_token), and all other
750                                        // parameters as specified at the start
751                                        // of this method.
752                                        self.#operation_id(
753                                            #(#step_params,)*
754                                        )
755                                        .map_ok(|page| {
756                                            let page = page.into_inner();
757                                            Some((
758                                                futures::stream::iter(
759                                                    page.items
760                                                ).map(Ok),
761                                                page.next_page,
762                                            ))
763                                        })
764                                        .await
765                                    }
766                                },
767                            )
768                            .try_flatten();
769
770                            first.chain(rest)
771                        })
772                        .try_flatten_stream()
773                        .boxed()
774                }
775            }
776        });
777
778        let all = quote! {
779            #method_impl
780            #stream_impl
781        };
782
783        Ok(all)
784    }
785
786    /// Common code generation between positional and builder interface-styles.
787    /// Returns a struct with the success and error types and the core body
788    /// implementation that marshals arguments and executes the request.
789    fn method_sig_body(
790        &self,
791        method: &OperationMethod,
792        client_type: TokenStream,
793        client_value: TokenStream,
794        has_inner: bool,
795    ) -> Result<MethodSigBody> {
796        let param_names = method
797            .params
798            .iter()
799            .map(|param| format_ident!("{}", param.name))
800            .collect::<Vec<_>>();
801
802        // Generate a unique Ident for internal variables
803        let url_ident = unique_ident_from("url", &param_names);
804        let request_ident = unique_ident_from("request", &param_names);
805        let response_ident = unique_ident_from("response", &param_names);
806        let result_ident = unique_ident_from("result", &param_names);
807
808        // Generate code for query parameters.
809        let query_params = method
810            .params
811            .iter()
812            .filter_map(|param| match &param.kind {
813                OperationParameterKind::Query(_) => {
814                    let qn = &param.api_name;
815                    let qn_ident = format_ident!("{}", &param.name);
816                    Some(quote! {
817                        &progenitor_middleware_client::QueryParam::new(#qn, &#qn_ident)
818                    })
819                }
820                _ => None,
821            })
822            .collect::<Vec<_>>();
823
824        let headers = method
825            .params
826            .iter()
827            .filter_map(|param| match &param.kind {
828                OperationParameterKind::Header(required) => {
829                    let hn = &param.api_name;
830                    let hn_ident = format_ident!("{}", &param.name);
831                    let res = if *required {
832                        quote! {
833                            header_map.append(
834                                #hn,
835                                #hn_ident.to_string().try_into()?
836                            );
837                        }
838                    } else {
839                        quote! {
840                            if let Some(value) = #hn_ident {
841                                header_map.append(
842                                    #hn,
843                                    value.to_string().try_into()?
844                                );
845                            }
846                        }
847                    };
848                    Some(res)
849                }
850                _ => None,
851            })
852            .collect::<Vec<_>>();
853
854        let headers_size = headers.len() + 1;
855        let headers_build = quote! {
856            let mut header_map = ::reqwest::header::HeaderMap::with_capacity(#headers_size);
857            header_map.append(
858                ::reqwest::header::HeaderName::from_static("api-version"),
859                ::reqwest::header::HeaderValue::from_static(#client_type::api_version()),
860            );
861
862            #(#headers)*
863        };
864
865        let headers_use = quote! {
866            .headers(header_map)
867        };
868
869        let websock_hdrs = if method.dropshot_websocket {
870            quote! {
871                .header(::reqwest::header::CONNECTION, "Upgrade")
872                .header(::reqwest::header::UPGRADE, "websocket")
873                .header(::reqwest::header::SEC_WEBSOCKET_VERSION, "13")
874                .header(
875                    ::reqwest::header::SEC_WEBSOCKET_KEY,
876                    ::base64::Engine::encode(
877                        &::base64::engine::general_purpose::STANDARD,
878                        ::rand::random::<[u8; 16]>(),
879                    )
880                )
881            }
882        } else {
883            quote! {}
884        };
885
886        // Generate the path rename map; then use it to generate code for
887        // assigning the path parameters to the `url` variable.
888        let url_renames = method
889            .params
890            .iter()
891            .filter_map(|param| match &param.kind {
892                OperationParameterKind::Path => Some((&param.api_name, &param.name)),
893                _ => None,
894            })
895            .collect();
896
897        let url_path = method.path.compile(url_renames, client_value.clone());
898        let url_path = quote! {
899            let #url_ident = #url_path;
900        };
901
902        // Generate code to handle the body param.
903        let body_func = method.params.iter().filter_map(|param| {
904            match (&param.kind, &param.typ) {
905                (
906                    OperationParameterKind::Body(BodyContentType::OctetStream),
907                    OperationParameterType::RawBody,
908                ) => Some(quote! {
909                    // Set the content type (this is handled by helper
910                    // functions for other MIME types).
911                    .header(
912                        ::reqwest::header::CONTENT_TYPE,
913                        ::reqwest::header::HeaderValue::from_static("application/octet-stream"),
914                    )
915                    .body(body)
916                }),
917                (
918                    OperationParameterKind::Body(BodyContentType::Text(mime_type)),
919                    OperationParameterType::RawBody,
920                ) => Some(quote! {
921                    // Set the content type (this is handled by helper
922                    // functions for other MIME types).
923                    .header(
924                        ::reqwest::header::CONTENT_TYPE,
925                        ::reqwest::header::HeaderValue::from_static(#mime_type),
926                    )
927                    .body(body)
928                }),
929                (
930                    OperationParameterKind::Body(BodyContentType::Json),
931                    OperationParameterType::Type(_),
932                ) => Some(quote! {
933                    // Serialization errors are deferred.
934                    .json(&body)
935                }),
936                (
937                    OperationParameterKind::Body(BodyContentType::FormUrlencoded),
938                    OperationParameterType::Type(_),
939                ) => Some(quote! {
940                    // This uses progenitor_middleware_client::RequestBuilderExt which
941                    // returns an error in the case of a serialization failure.
942                    .form_urlencoded(&body)?
943                }),
944                (OperationParameterKind::Body(_), _) => {
945                    unreachable!("invalid body kind/type combination")
946                }
947                _ => None,
948            }
949        });
950        // ... and there can be at most one body.
951        assert!(body_func.clone().count() <= 1);
952
953        let (success_response_items, response_type) =
954            self.extract_responses(method, OperationResponseStatus::is_success_or_default);
955
956        // Errors... (get error responses early so we can use the type for annotations in success responses)
957        let (error_response_items, error_response_type) =
958            self.extract_responses(method, OperationResponseStatus::is_error_or_default);
959
960        // Get the error type tokens for type annotations in success responses
961        let error_type_annotation = match &error_response_type {
962            ErrorResponseType::Single(kind) => {
963                let error_tokens = kind.clone().into_tokens(&self.type_space);
964                quote! { #error_tokens }
965            }
966            ErrorResponseType::Multiple { enum_name, .. } => {
967                let enum_ident = format_ident!("{}", enum_name);
968                quote! { types::#enum_ident }
969            }
970        };
971
972        let success_response_matches = success_response_items.iter().map(|response| {
973            let pat = match &response.status_code {
974                OperationResponseStatus::Code(code) => quote! { #code },
975                OperationResponseStatus::Range(_) | OperationResponseStatus::Default => {
976                    quote! { 200 ..= 299 }
977                }
978            };
979
980            let decode = match &response.typ {
981                OperationResponseKind::Type(_) => {
982                    quote! {
983                        ResponseValue::from_response::<#error_type_annotation>(#response_ident).await
984                    }
985                }
986                OperationResponseKind::None => {
987                    quote! {
988                        Ok(ResponseValue::empty(#response_ident))
989                    }
990                }
991                OperationResponseKind::Raw => {
992                    quote! {
993                        Ok(ResponseValue::stream(#response_ident))
994                    }
995                }
996                OperationResponseKind::Upgrade => {
997                    quote! {
998                        ResponseValue::upgrade::<#error_type_annotation>(#response_ident).await
999                    }
1000                }
1001            };
1002
1003            quote! { #pat => { #decode } }
1004        });
1005
1006        // Helper closure defined here (before use) - error_response_items and error_response_type already retrieved above
1007        let generate_error_match_arm = |response: &OperationResponse,
1008                                        enum_info: Option<(&str, &str)>|
1009         -> TokenStream {
1010            let pat = match &response.status_code {
1011                OperationResponseStatus::Code(code) => {
1012                    quote! { #code }
1013                }
1014                OperationResponseStatus::Range(r) => {
1015                    let min = r * 100;
1016                    let max = min + 99;
1017                    quote! { #min ..= #max }
1018                }
1019                OperationResponseStatus::Default => {
1020                    quote! { _ }
1021                }
1022            };
1023
1024            let decode = match &response.typ {
1025                OperationResponseKind::Type(type_id) => {
1026                    if let Some((enum_name, variant_name)) = enum_info {
1027                        let enum_ident = format_ident!("{}", enum_name);
1028                        let variant_ident = format_ident!("{}", variant_name);
1029                        // Get the inner type for this error response
1030                        let inner_type_tokens = response.typ.clone().into_tokens(&self.type_space);
1031
1032                        quote! {
1033                            {
1034                                let inner_value: ResponseValue<#inner_type_tokens> = ResponseValue::from_response::<types::#enum_ident>(#response_ident).await?;
1035                                let status = inner_value.status();
1036                                let headers = inner_value.headers().clone();
1037                                let inner = inner_value.into_inner();
1038                                Err(Error::ErrorResponse(
1039                                    ResponseValue::new(
1040                                        types::#enum_ident::#variant_ident(inner),
1041                                        status,
1042                                        headers
1043                                    )
1044                                ))
1045                            }
1046                        }
1047                    } else {
1048                        quote! {
1049                            Err(Error::ErrorResponse(
1050                                ResponseValue::from_response(#response_ident).await?
1051                            ))
1052                        }
1053                    }
1054                }
1055                OperationResponseKind::None => {
1056                    if let Some((enum_name, variant_name)) = enum_info {
1057                        let enum_ident = format_ident!("{}", enum_name);
1058                        let variant_ident = format_ident!("{}", variant_name);
1059                        quote! {
1060                            {
1061                                let inner_value = ResponseValue::empty(#response_ident);
1062                                let status = inner_value.status();
1063                                let headers = inner_value.headers().clone();
1064                                Err(Error::ErrorResponse(
1065                                    ResponseValue::new(
1066                                        types::#enum_ident::#variant_ident,
1067                                        status,
1068                                        headers
1069                                    )
1070                                ))
1071                            }
1072                        }
1073                    } else {
1074                        quote! {
1075                            Err(Error::ErrorResponse(
1076                                ResponseValue::empty(#response_ident)
1077                            ))
1078                        }
1079                    }
1080                }
1081                OperationResponseKind::Raw => {
1082                    if let Some((enum_name, variant_name)) = enum_info {
1083                        let enum_ident = format_ident!("{}", enum_name);
1084                        let variant_ident = format_ident!("{}", variant_name);
1085                        quote! {
1086                            {
1087                                let inner_value = ResponseValue::stream(#response_ident);
1088                                let status = inner_value.status();
1089                                let headers = inner_value.headers().clone();
1090                                let inner = inner_value.into_inner_stream();
1091                                Err(Error::ErrorResponse(
1092                                    ResponseValue::new(
1093                                        types::#enum_ident::#variant_ident(inner),
1094                                        status,
1095                                        headers
1096                                    )
1097                                ))
1098                            }
1099                        }
1100                    } else {
1101                        quote! {
1102                            Err(Error::ErrorResponse(
1103                                ResponseValue::stream(#response_ident)
1104                            ))
1105                        }
1106                    }
1107                }
1108                OperationResponseKind::Upgrade => {
1109                    if response.status_code == OperationResponseStatus::Default {
1110                        return quote! {}; // catch-all handled below
1111                    } else {
1112                        todo!(
1113                            "non-default error response handling for \
1114                                    upgrade requests is not yet implemented"
1115                        );
1116                    }
1117                }
1118            };
1119
1120            quote! { #pat => { #decode } }
1121        };
1122
1123        let error_response_matches = match &error_response_type {
1124            ErrorResponseType::Single(_) => {
1125                // Single error type - generate existing style matches
1126                error_response_items
1127                    .iter()
1128                    .map(|response| generate_error_match_arm(response, None))
1129                    .collect::<Vec<_>>()
1130            }
1131            ErrorResponseType::Multiple {
1132                enum_name,
1133                variants,
1134            } => {
1135                // Multiple error types - wrap in enum variants
1136                error_response_items
1137                    .iter()
1138                    .map(|response| {
1139                        // Find which variant this response belongs to
1140                        let variant = variants
1141                            .iter()
1142                            .find(|v| v.status_codes.contains(&response.status_code))
1143                            .expect("Response must map to a variant");
1144
1145                        generate_error_match_arm(
1146                            response,
1147                            Some((enum_name.as_str(), variant.variant_name.as_str())),
1148                        )
1149                    })
1150                    .collect::<Vec<_>>()
1151            }
1152        };
1153
1154        let accept_header = {
1155            let has_error_type = match &error_response_type {
1156                ErrorResponseType::Single(kind) => matches!(kind, OperationResponseKind::Type(_)),
1157                ErrorResponseType::Multiple { .. } => true, // Enums always need JSON accept
1158            };
1159
1160            let has_success_type = match &response_type {
1161                ErrorResponseType::Single(kind) => matches!(kind, OperationResponseKind::Type(_)),
1162                ErrorResponseType::Multiple { .. } => true,
1163            };
1164            let has_success_none = match &response_type {
1165                ErrorResponseType::Single(kind) => matches!(kind, OperationResponseKind::None),
1166                ErrorResponseType::Multiple { .. } => false,
1167            };
1168
1169            has_success_type || (has_success_none && has_error_type)
1170        }
1171        .then(|| {
1172            quote! {
1173                    .header(
1174                        ::reqwest::header::ACCEPT,
1175                        ::reqwest::header::HeaderValue::from_static(
1176                            "application/json",
1177                        ),
1178                    )
1179            }
1180        });
1181
1182        // Generate the catch-all case for other statuses. If the operation
1183        // specifies a default response, we've already generated a default
1184        // match as part of error response code handling. (And we've handled
1185        // the default as a success response as well.) Otherwise the catch-all
1186        // produces an error corresponding to a response not specified in the
1187        // API description.
1188        let default_response = match method.responses.iter().last() {
1189            Some(response) if response.status_code.is_default() => quote! {},
1190            _ => {
1191                quote! { _ => Err(Error::UnexpectedResponse(#response_ident)), }
1192            }
1193        };
1194
1195        let inner = match has_inner {
1196            true => quote! { &#client_value.inner, },
1197            false => quote! {},
1198        };
1199        let pre_hook = self.settings.pre_hook.as_ref().map(|hook| {
1200            quote! {
1201                (#hook)(#inner &#request_ident);
1202            }
1203        });
1204        let pre_hook_async = self.settings.pre_hook_async.as_ref().map(|hook| {
1205            quote! {
1206                match (#hook)(#inner &mut #request_ident).await {
1207                    Ok(_) => (),
1208                    Err(e) => return Err(Error::Custom(e.to_string())),
1209                }
1210            }
1211        });
1212        let post_hook = self.settings.post_hook.as_ref().map(|hook| {
1213            quote! {
1214                (#hook)(#inner &#result_ident);
1215            }
1216        });
1217        let post_hook_async = self.settings.post_hook_async.as_ref().map(|hook| {
1218            quote! {
1219                match (#hook)(#inner &#result_ident).await {
1220                    Ok(_) => (),
1221                    Err(e) => return Err(Error::Custom(e.to_string())),
1222                }
1223            }
1224        });
1225
1226        let operation_id = &method.operation_id;
1227        let method_func = format_ident!("{}", method.method.as_str());
1228
1229        let body_impl = quote! {
1230            #url_path
1231
1232            #headers_build
1233
1234            #[allow(unused_mut)]
1235            let mut #request_ident = #client_value.client
1236                . #method_func (#url_ident)
1237                #accept_header
1238                #(#body_func)*
1239                #( .query(#query_params) )*
1240                #headers_use
1241                #websock_hdrs
1242                .build()?;
1243
1244            let info = OperationInfo {
1245                operation_id: #operation_id,
1246            };
1247
1248            #pre_hook
1249            #pre_hook_async
1250            #client_value
1251                .pre(&mut #request_ident, &info)
1252                .await?;
1253
1254            let #result_ident = #client_value
1255                .exec(#request_ident, &info)
1256                .await;
1257
1258            #client_value
1259                .post(&#result_ident, &info)
1260                .await?;
1261            #post_hook_async
1262            #post_hook
1263
1264            let #response_ident = #result_ident?;
1265
1266            match #response_ident.status().as_u16() {
1267                // These will be of the form...
1268                // 201 => ResponseValue::from_response(response).await,
1269                // 200..299 => ResponseValue::empty(response),
1270                // TODO this kind of enumerated response isn't implemented
1271                // ... or in the case of an operation with multiple
1272                // successful response types...
1273                // 200 => {
1274                //     ResponseValue::from_response()
1275                //         .await?
1276                //         .map(OperationXResponse::ResponseTypeA)
1277                // }
1278                // 201 => {
1279                //     ResponseValue::from_response()
1280                //         .await?
1281                //         .map(OperationXResponse::ResponseTypeB)
1282                // }
1283                #(#success_response_matches)*
1284
1285                // This is almost identical to the success types except
1286                // they are wrapped in Error::ErrorResponse...
1287                // 400 => {
1288                //     Err(Error::ErrorResponse(
1289                //         ResponseValue::from_response(response.await?)
1290                //     ))
1291                // }
1292                #(#error_response_matches)*
1293
1294                // The default response is either an Error with a known
1295                // type if the operation defines a default (as above) or
1296                // an Error::UnexpectedResponse...
1297                // _ => Err(Error::UnexpectedResponse(response)),
1298                #default_response
1299            }
1300        };
1301
1302        let error_type_tokens = match &error_response_type {
1303            ErrorResponseType::Single(kind) => kind.clone().into_tokens(&self.type_space),
1304            ErrorResponseType::Multiple { enum_name, .. } => {
1305                let enum_ident = format_ident!("{}", enum_name);
1306                quote! { types::#enum_ident }
1307            }
1308        };
1309
1310        let success_type_tokens = match &response_type {
1311            ErrorResponseType::Single(kind) => kind.clone().into_tokens(&self.type_space),
1312            ErrorResponseType::Multiple { enum_name, .. } => {
1313                let enum_ident = format_ident!("{}", enum_name);
1314                quote! { types::#enum_ident }
1315            }
1316        };
1317
1318        Ok(MethodSigBody {
1319            success: success_type_tokens,
1320            error: error_type_tokens,
1321            body: body_impl,
1322        })
1323    }
1324
1325    /// Extract responses that match criteria specified by the `filter`. The
1326    /// result is a `Vec<OperationResponse>` that enumerates the cases matching
1327    /// the filter, and an `ErrorResponseType` that represents the generated type(s)
1328    /// for those cases.
1329    pub fn extract_responses<'a>(
1330        &self,
1331        method: &'a OperationMethod,
1332        filter: fn(&OperationResponseStatus) -> bool,
1333    ) -> (Vec<&'a OperationResponse>, ErrorResponseType) {
1334        let mut response_items = method
1335            .responses
1336            .iter()
1337            .filter(|response| filter(&response.status_code))
1338            .collect::<Vec<_>>();
1339        response_items.sort();
1340
1341        // If we have a success range and a default, we can pop off the default
1342        // since it will never be hit. Note that this is a no-op for error
1343        // responses.
1344        let len = response_items.len();
1345        if len >= 2 {
1346            if let (
1347                OperationResponse {
1348                    status_code: OperationResponseStatus::Range(2),
1349                    ..
1350                },
1351                OperationResponse {
1352                    status_code: OperationResponseStatus::Default,
1353                    ..
1354                },
1355            ) = (&response_items[len - 2], &response_items[len - 1])
1356            {
1357                response_items.pop();
1358            }
1359        }
1360
1361        let response_types = response_items
1362            .iter()
1363            .map(|response| response.typ.clone())
1364            .collect::<BTreeSet<_>>();
1365
1366        // Handle single error type (backwards compatible)
1367        if response_types.len() <= 1 {
1368            let response_type = response_types
1369                .into_iter()
1370                .next()
1371                .unwrap_or(OperationResponseKind::None);
1372            return (response_items, ErrorResponseType::Single(response_type));
1373        }
1374
1375        // Multiple error types: create enum
1376        let enum_name = format!("{}Error", sanitize(&method.operation_id, Case::Pascal));
1377        let variants = self.create_error_variants(&response_items);
1378
1379        (
1380            response_items,
1381            ErrorResponseType::Multiple {
1382                enum_name,
1383                variants,
1384            },
1385        )
1386    }
1387
1388    /// Generates an error enum type for operations with multiple error response types.
1389    pub fn generate_error_enum(&self, enum_name: &str, variants: &[ErrorVariant]) -> TokenStream {
1390        let enum_ident = format_ident!("{}", enum_name);
1391
1392        let variant_defs = variants.iter().map(|v| {
1393            let variant_ident = format_ident!("{}", v.variant_name);
1394
1395            match &v.typ {
1396                OperationResponseKind::Type(_) => {
1397                    // Get the type tokens which include "types::" prefix
1398                    let typ_tokens = v.typ.clone().into_tokens(&self.type_space);
1399
1400                    // Convert to string and remove "types::" prefix since we're inside the types module
1401                    let typ_string = typ_tokens.to_string();
1402                    let typ_without_prefix =
1403                        typ_string.strip_prefix("types :: ").unwrap_or(&typ_string);
1404
1405                    // Parse back to TokenStream
1406                    let typ_tokens: TokenStream = typ_without_prefix.parse().expect(&format!(
1407                        "Failed to parse type tokens: {}",
1408                        typ_without_prefix
1409                    ));
1410
1411                    quote! { #variant_ident(#typ_tokens) }
1412                }
1413                OperationResponseKind::None => {
1414                    quote! { #variant_ident }
1415                }
1416                OperationResponseKind::Raw => {
1417                    // ByteStream is imported into types module from super
1418                    quote! { #variant_ident(ByteStream) }
1419                }
1420                OperationResponseKind::Upgrade => {
1421                    quote! { #variant_ident(reqwest::Upgraded) }
1422                }
1423            }
1424        });
1425
1426        quote! {
1427            #[derive(Debug)]
1428            pub enum #enum_ident {
1429                #(#variant_defs,)*
1430            }
1431        }
1432    }
1433
1434    /// Creates enum variants from a list of error responses, creating one variant per status code.
1435    fn create_error_variants(&self, responses: &[&OperationResponse]) -> Vec<ErrorVariant> {
1436        let mut variants = Vec::new();
1437
1438        // Create one variant per response, even if types are duplicated
1439        for response in responses {
1440            let variant_name = match &response.status_code {
1441                OperationResponseStatus::Code(code) => format!("Status{}", code),
1442                OperationResponseStatus::Range(r) => format!("Status{}xx", r),
1443                OperationResponseStatus::Default => "DefaultResponse".to_string(),
1444            };
1445
1446            variants.push(ErrorVariant {
1447                variant_name,
1448                typ: response.typ.clone(),
1449                status_codes: vec![response.status_code.clone()],
1450            });
1451        }
1452
1453        variants
1454    }
1455
1456    // Validates all the necessary conditions for Dropshot pagination. Returns
1457    // the paginated item type data if all conditions are met.
1458    fn dropshot_pagination_data(
1459        &self,
1460        operation: &openapiv3::Operation,
1461        parameters: &[OperationParameter],
1462        responses: &[OperationResponse],
1463    ) -> Option<DropshotPagination> {
1464        let value = operation.extensions.get("x-dropshot-pagination")?;
1465
1466        // We expect to see at least "page_token" and "limit" parameters.
1467        if parameters
1468            .iter()
1469            .filter(|param| {
1470                matches!(
1471                    (param.api_name.as_str(), &param.kind),
1472                    ("page_token", OperationParameterKind::Query(false))
1473                        | ("limit", OperationParameterKind::Query(false))
1474                )
1475            })
1476            .count()
1477            != 2
1478        {
1479            return None;
1480        }
1481
1482        // All query parameters must be optional since page_token may not be
1483        // specified in conjunction with other query parameters.
1484        if !parameters.iter().all(|param| match &param.kind {
1485            OperationParameterKind::Query(required) => !required,
1486            _ => true,
1487        }) {
1488            return None;
1489        }
1490
1491        // A raw body parameter can only be passed to a single call as it may
1492        // be a streaming type. We can't use a streaming type for a paginated
1493        // interface because we can only stream it once rather than for the
1494        // multiple calls required to collect all pages.
1495        if parameters
1496            .iter()
1497            .any(|param| param.typ == OperationParameterType::RawBody)
1498        {
1499            return None;
1500        }
1501
1502        // There must be exactly one successful response type.
1503        let mut success_response_items =
1504            responses
1505                .iter()
1506                .filter_map(|response| match (&response.status_code, &response.typ) {
1507                    (
1508                        OperationResponseStatus::Code(200..=299)
1509                        | OperationResponseStatus::Range(2),
1510                        OperationResponseKind::Type(type_id),
1511                    ) => Some(type_id),
1512                    _ => None,
1513                });
1514
1515        let success_response = match (success_response_items.next(), success_response_items.next())
1516        {
1517            (None, _) | (_, Some(_)) => return None,
1518            (Some(success), None) => success,
1519        };
1520
1521        let typ = self.type_space.get_type(success_response).ok()?;
1522        let details = match typ.details() {
1523            typify::TypeDetails::Struct(details) => details,
1524            _ => return None,
1525        };
1526
1527        let properties = details.properties().collect::<BTreeMap<_, _>>();
1528
1529        // There should be exactly two properties: items and next_page
1530        if properties.len() != 2 {
1531            return None;
1532        }
1533
1534        // We need a next_page property that's an Option<String>.
1535        if let typify::TypeDetails::Option(ref opt_id) = self
1536            .type_space
1537            .get_type(properties.get("next_page")?)
1538            .ok()?
1539            .details()
1540        {
1541            if !matches!(
1542                self.type_space.get_type(opt_id).ok()?.details(),
1543                typify::TypeDetails::String
1544            ) {
1545                return None;
1546            }
1547        } else {
1548            return None;
1549        }
1550
1551        match self
1552            .type_space
1553            .get_type(properties.get("items")?)
1554            .ok()?
1555            .details()
1556        {
1557            typify::TypeDetails::Vec(item) => {
1558                #[derive(serde::Deserialize, Default)]
1559                struct DropshotPaginationFormat {
1560                    required: Vec<String>,
1561                }
1562                let first_page_params =
1563                    serde_json::from_value::<DropshotPaginationFormat>(value.clone())
1564                        .unwrap_or_default()
1565                        .required;
1566                Some(DropshotPagination {
1567                    item,
1568                    first_page_params,
1569                })
1570            }
1571            _ => None,
1572        }
1573    }
1574
1575    /// Create the builder structs along with their impl bodies.
1576    ///
1577    /// Builder structs are generally of this form for a mandatory `param_1`
1578    /// and an optional `param_2`:
1579    /// ```ignore
1580    /// struct OperationId<'a> {
1581    ///     client: &'a super::Client,
1582    ///     param_1: Result<SomeType, String>,
1583    ///     param_2: Result<Option<String>, String>,
1584    /// }
1585    /// ```
1586    ///
1587    /// All parameters are present and all their types are `Result<T, String>`
1588    /// or `Result<Option<T>, String>` for optional parameters. Each parameter
1589    /// also has a corresponding method:
1590    /// ```ignore
1591    /// impl<'a> OperationId<'a> {
1592    ///     pub fn param_1<V>(self, value: V)
1593    ///         where V: std::convert::TryInto<SomeType>
1594    ///     {
1595    ///         self.param_1 = value.try_into()
1596    ///             .map_err(|_| #err_msg.to_string());
1597    ///         self
1598    ///     }
1599    ///     pub fn param_2<V>(self, value: V)
1600    ///         where V: std::convert::TryInto<SomeType>
1601    ///     {
1602    ///         self.param_2 = value.try_into()
1603    ///             .map(Some)
1604    ///             .map_err(|_| #err_msg.to_string());
1605    ///         self
1606    ///     }
1607    /// }
1608    /// ```
1609    ///
1610    /// The Client's operation_id method simply invokes the builder's new
1611    /// method, which assigns an error value to mandatory field and a
1612    /// `Ok(None)` value to optional ones:
1613    /// ```ignore
1614    /// impl<'a> OperationId<'a> {
1615    ///     pub fn new(client: &'a super::Client) -> Self {
1616    ///         Self {
1617    ///             client,
1618    ///             param_1: Err("param_1 was not initialized".to_string()),
1619    ///             param_2: Ok(None),
1620    ///         }
1621    ///     }
1622    /// }
1623    /// ```
1624    ///
1625    /// Finally, builders have methods to execute the operation. This simply
1626    /// resolves each parameter with the ? (`Try` operator).
1627    /// ```ignore
1628    /// impl<'a> OperationId<'a> {
1629    ///     pub fn send(self) -> Result<
1630    ///         ResponseValue<SuccessType>,
1631    ///         Error<ErrorType>,
1632    ///     > {
1633    ///         let Self {
1634    ///             client,
1635    ///             param_1,
1636    ///             param_2,
1637    ///         } = self;
1638    ///
1639    ///         let param_1 = param_1.map_err(Error::InvalidRequest)?;
1640    ///         let param_2 = param_1.map_err(Error::InvalidRequest)?;
1641    ///
1642    ///         // ... execute the body (see `method_sig_body`) ...
1643    ///     }
1644    /// }
1645    /// ```
1646    ///
1647    /// Finally, paginated interfaces have a `stream()` method which uses the
1648    /// `send()` method above to fetch each page of results to assemble the
1649    /// items into a single `impl Stream`.
1650    pub(crate) fn builder_struct(
1651        &mut self,
1652        method: &OperationMethod,
1653        tag_style: TagStyle,
1654        has_inner: bool,
1655    ) -> Result<TokenStream> {
1656        let struct_name = sanitize(&method.operation_id, Case::Pascal);
1657        let struct_ident = format_ident!("{}", struct_name);
1658
1659        // Generate an ident for each parameter.
1660        let param_names = method
1661            .params
1662            .iter()
1663            .map(|param| format_ident!("{}", param.name))
1664            .collect::<Vec<_>>();
1665
1666        let client_ident = unique_ident_from("client", &param_names);
1667
1668        let mut cloneable = true;
1669
1670        // Generate the type for each parameter.
1671        let param_types = method
1672            .params
1673            .iter()
1674            .map(|param| match &param.typ {
1675                OperationParameterType::Type(type_id) => {
1676                    let ty = self.type_space.get_type(type_id)?;
1677
1678                    // For body parameters only, if there's a builder we'll
1679                    // nest that within this builder.
1680                    if let (OperationParameterKind::Body(_), Some(builder_name)) =
1681                        (&param.kind, ty.builder())
1682                    {
1683                        Ok(quote! { Result<#builder_name, String> })
1684                    } else if param.kind.is_required() {
1685                        let t = ty.ident();
1686                        Ok(quote! { Result<#t, String> })
1687                    } else {
1688                        let t = ty.ident();
1689                        Ok(quote! { Result<Option<#t>, String> })
1690                    }
1691                }
1692
1693                OperationParameterType::RawBody => {
1694                    cloneable = false;
1695                    Ok(quote! { Result<reqwest::Body, String> })
1696                }
1697            })
1698            .collect::<Result<Vec<_>>>()?;
1699
1700        // Generate the default value value for each parameter. For optional
1701        // parameters it's just `Ok(None)`. For builders it's
1702        // `Ok(Default::default())`. For required, non-builders it's an Err(_)
1703        // that indicates which field isn't initialized.
1704        let param_values = method
1705            .params
1706            .iter()
1707            .map(|param| match &param.typ {
1708                OperationParameterType::Type(type_id) => {
1709                    let ty = self.type_space.get_type(type_id)?;
1710
1711                    // Fill in the appropriate initial value for the
1712                    // param_types generated above.
1713                    if let (OperationParameterKind::Body(_), Some(_)) = (&param.kind, ty.builder())
1714                    {
1715                        Ok(quote! { Ok(::std::default::Default::default()) })
1716                    } else if param.kind.is_required() {
1717                        let err_msg = format!("{} was not initialized", param.name);
1718                        Ok(quote! { Err(#err_msg.to_string()) })
1719                    } else {
1720                        Ok(quote! { Ok(None) })
1721                    }
1722                }
1723
1724                OperationParameterType::RawBody => {
1725                    let err_msg = format!("{} was not initialized", param.name);
1726                    Ok(quote! { Err(#err_msg.to_string()) })
1727                }
1728            })
1729            .collect::<Result<Vec<_>>>()?;
1730
1731        // For builders we map `Ok` values to perform a `try_from` to attempt
1732        // to convert the builder into the desired type. No "finalization" is
1733        // required for non-builders (required or optional).
1734        let param_finalize = method
1735            .params
1736            .iter()
1737            .map(|param| match &param.typ {
1738                OperationParameterType::Type(type_id) => {
1739                    let ty = self.type_space.get_type(type_id)?;
1740                    if ty.builder().is_some() {
1741                        let type_name = ty.ident();
1742                        Ok(quote! {
1743                            .and_then(|v| #type_name::try_from(v)
1744                                .map_err(|e| e.to_string()))
1745                        })
1746                    } else {
1747                        Ok(quote! {})
1748                    }
1749                }
1750                OperationParameterType::RawBody => Ok(quote! {}),
1751            })
1752            .collect::<Result<Vec<_>>>()?;
1753
1754        // For each parameter, we need an impl for the builder to let consumers
1755        // provide a value.
1756        let param_impls = method
1757            .params
1758            .iter()
1759            .map(|param| {
1760                let param_name = format_ident!("{}", param.name);
1761                match &param.typ {
1762                    OperationParameterType::Type(type_id) => {
1763                        let ty = self.type_space.get_type(type_id)?;
1764                        match (ty.builder(), param.kind.is_optional()) {
1765                            // TODO right now optional body parameters are not
1766                            // addressed
1767                            (Some(_), true) => {
1768                                unreachable!()
1769                            }
1770                            (None, true) => {
1771                                let typ = ty.ident();
1772                                let err_msg = format!(
1773                                    "conversion to `{}` for {} failed",
1774                                    ty.name(),
1775                                    param.name,
1776                                );
1777                                Ok(quote! {
1778                                    pub fn #param_name<V>(
1779                                        mut self,
1780                                        value: V,
1781                                    ) -> Self
1782                                        where V: std::convert::TryInto<#typ>,
1783                                    {
1784                                        self.#param_name = value.try_into()
1785                                            .map(Some)
1786                                            .map_err(|_| #err_msg.to_string());
1787                                        self
1788                                    }
1789                                })
1790                            }
1791                            (None, false) => {
1792                                let typ = ty.ident();
1793                                let err_msg = format!(
1794                                    "conversion to `{}` for {} failed",
1795                                    ty.name(),
1796                                    param.name,
1797                                );
1798                                Ok(quote! {
1799                                    pub fn #param_name<V>(
1800                                        mut self,
1801                                        value: V,
1802                                    ) -> Self
1803                                        where V: std::convert::TryInto<#typ>,
1804                                    {
1805                                        self.#param_name = value.try_into()
1806                                            .map_err(|_| #err_msg.to_string());
1807                                        self
1808                                    }
1809                                })
1810                            }
1811
1812                            // For builder-capable bodies we offer a `body()`
1813                            // method that sets the full body (by constructing
1814                            // a builder **from** the body type). We also offer
1815                            // a `body_map()` method that operates on the
1816                            // builder itself.
1817                            (Some(builder_name), false) => {
1818                                assert_eq!(param.name, "body");
1819                                let typ = ty.ident();
1820                                let err_msg = format!(
1821                                    "conversion to `{}` for {} failed: {{}}",
1822                                    ty.name(),
1823                                    param.name,
1824                                );
1825                                Ok(quote! {
1826                                    pub fn body<V>(mut self, value: V) -> Self
1827                                    where
1828                                        V: std::convert::TryInto<#typ>,
1829                                        <V as std::convert::TryInto<#typ>>::Error:
1830                                            std::fmt::Display,
1831                                    {
1832                                        self.body = value.try_into()
1833                                            .map(From::from)
1834                                            .map_err(|s| format!(#err_msg, s));
1835                                        self
1836                                    }
1837
1838                                    pub fn body_map<F>(mut self, f: F) -> Self
1839                                    where
1840                                        F: std::ops::FnOnce(#builder_name)
1841                                            -> #builder_name,
1842                                    {
1843                                        self.body = self.body.map(f);
1844                                        self
1845                                    }
1846                                })
1847                            }
1848                        }
1849                    }
1850
1851                    OperationParameterType::RawBody => match param.kind {
1852                        OperationParameterKind::Body(BodyContentType::OctetStream) => {
1853                            let err_msg =
1854                                format!("conversion to `reqwest::Body` for {} failed", param.name,);
1855
1856                            Ok(quote! {
1857                                pub fn #param_name<B>(mut self, value: B) -> Self
1858                                    where B: std::convert::TryInto<reqwest::Body>
1859                                {
1860                                    self.#param_name = value.try_into()
1861                                        .map_err(|_| #err_msg.to_string());
1862                                    self
1863                                }
1864                            })
1865                        }
1866                        OperationParameterKind::Body(BodyContentType::Text(_)) => {
1867                            let err_msg =
1868                                format!("conversion to `String` for {} failed", param.name,);
1869
1870                            Ok(quote! {
1871                                pub fn #param_name<V>(mut self, value: V) -> Self
1872                                    where V: std::convert::TryInto<String>
1873                                {
1874                                    self.#param_name = value
1875                                        .try_into()
1876                                        .map_err(|_| #err_msg.to_string())
1877                                        .map(|v| v.into());
1878                                    self
1879                                }
1880                            })
1881                        }
1882                        _ => unreachable!(),
1883                    },
1884                }
1885            })
1886            .collect::<Result<Vec<_>>>()?;
1887
1888        let MethodSigBody {
1889            success,
1890            error,
1891            body,
1892        } = self.method_sig_body(
1893            method,
1894            quote! { super::Client },
1895            quote! { #client_ident },
1896            has_inner,
1897        )?;
1898
1899        let send_doc = format!(
1900            "Sends a `{}` request to `{}`",
1901            method.method.as_str().to_ascii_uppercase(),
1902            method.path.to_string(),
1903        );
1904        let send_impl = quote! {
1905            #[doc = #send_doc]
1906            pub async fn send(self) -> Result<
1907                ResponseValue<#success>,
1908                Error<#error>,
1909            > {
1910                // Destructure the builder for convenience.
1911                let Self {
1912                    #client_ident,
1913                    #( #param_names, )*
1914                } = self;
1915
1916                // Extract parameters into variables, returning an error if
1917                // a value has not been provided or there was a conversion
1918                // error.
1919                //
1920                // TODO we could do something a bit nicer by collecting all
1921                // errors rather than just reporting the first one.
1922                #(
1923                let #param_names =
1924                    #param_names
1925                        #param_finalize
1926                        .map_err(Error::InvalidRequest)?;
1927                )*
1928
1929                // Do the work.
1930                #body
1931            }
1932        };
1933
1934        let stream_impl = method.dropshot_paginated.as_ref().map(|page_data| {
1935            // We're now using futures.
1936            self.uses_futures = true;
1937
1938            let step_params = method.params.iter().filter_map(|param| {
1939                if param.api_name.as_str() != "limit"
1940                    && matches!(param.kind, OperationParameterKind::Query(_))
1941                {
1942                    // Query parameters (other than "limit") are None; having
1943                    // page_token as Some(_), as we will during the loop below,
1944                    // is mutually exclusive with other query parameters.
1945                    let name = format_ident!("{}", param.name);
1946                    Some(quote! {
1947                        #name: Ok(None)
1948                    })
1949                } else {
1950                    None
1951                }
1952            });
1953
1954            // The item type that we've saved (by picking apart the original
1955            // function's return type) will be the Item type parameter for the
1956            // Stream impl we return.
1957            let item = self.type_space.get_type(&page_data.item).unwrap();
1958            let item_type = item.ident();
1959
1960            let stream_doc = format!(
1961                "Streams `{}` requests to `{}`",
1962                method.method.as_str().to_ascii_uppercase(),
1963                method.path.to_string(),
1964            );
1965
1966            quote! {
1967                #[doc = #stream_doc]
1968                pub fn stream(self) -> impl futures::Stream<Item = Result<
1969                    #item_type,
1970                    Error<#error>,
1971                >> + Unpin + 'a {
1972                    use ::futures::StreamExt;
1973                    use ::futures::TryFutureExt;
1974                    use ::futures::TryStreamExt;
1975
1976                    // This is the builder template we'll use for iterative
1977                    // steps past the first; it has all query params set to
1978                    // None (the step will fill in page_token).
1979                    let next = Self {
1980                        #( #step_params, )*
1981                        ..self.clone()
1982                    };
1983
1984                    self.send()
1985                        .map_ok(move |page| {
1986                            let page = page.into_inner();
1987
1988                            // Create a stream from the first page of items.
1989                            let first =
1990                                futures::stream::iter(page.items).map(Ok);
1991
1992                            // We unfold subsequent pages using page.next_page
1993                            // as the seed value. Each iteration returns its
1994                            // items and the new state which is a tuple of the
1995                            // next page token and the Self template.
1996                            let rest = futures::stream::try_unfold(
1997                                (page.next_page, next),
1998                                |(next_page, next)| async {
1999                                    if next_page.is_none() {
2000                                        // The page_token was None so we've
2001                                        // reached the end.
2002                                        Ok(None)
2003                                    } else {
2004                                        // Get the next page using the next
2005                                        // template (with query parameters set
2006                                        // to None), overriding page_token.
2007                                        Self {
2008                                            page_token: Ok(next_page),
2009                                            ..next.clone()
2010                                        }
2011                                        .send()
2012                                        .map_ok(|page| {
2013                                            let page = page.into_inner();
2014                                            Some((
2015                                                futures::stream::iter(
2016                                                    page.items
2017                                                ).map(Ok),
2018                                                (page.next_page, next),
2019                                            ))
2020                                        })
2021                                        .await
2022                                    }
2023                                },
2024                            )
2025                            .try_flatten();
2026
2027                            first.chain(rest)
2028                        })
2029                        .try_flatten_stream()
2030                        .boxed()
2031                }
2032            }
2033        });
2034
2035        let mut derives = vec![quote! { Debug }];
2036        if cloneable {
2037            derives.push(quote! { Clone });
2038        }
2039
2040        let derive = quote! {
2041            #[derive( #( #derives ),* )]
2042        };
2043
2044        // Build a reasonable doc comment depending on whether this struct is
2045        // the output from
2046        // 1. A Client method
2047        // 2. An extension trait method
2048        // 3. Several extension trait methods
2049        let struct_doc = match (tag_style, method.tags.len(), method.tags.first()) {
2050            (TagStyle::Merged, _, _) | (TagStyle::Separate, 0, _) => {
2051                let ty = format!("Client::{}", method.operation_id);
2052                format!("Builder for [`{}`]\n\n[`{}`]: super::{}", ty, ty, ty,)
2053            }
2054            (TagStyle::Separate, 1, Some(tag)) => {
2055                let ty = format!(
2056                    "Client{}Ext::{}",
2057                    sanitize(tag, Case::Pascal),
2058                    method.operation_id
2059                );
2060                format!("Builder for [`{}`]\n\n[`{}`]: super::{}", ty, ty, ty,)
2061            }
2062            (TagStyle::Separate, _, _) => {
2063                format!(
2064                    "Builder for `{}` operation\n\nSee {}\n\n{}",
2065                    method.operation_id,
2066                    method
2067                        .tags
2068                        .iter()
2069                        .map(|tag| {
2070                            format!(
2071                                "[`Client{}Ext::{}`]",
2072                                sanitize(tag, Case::Pascal),
2073                                method.operation_id,
2074                            )
2075                        })
2076                        .collect::<Vec<_>>()
2077                        .join(", "),
2078                    method
2079                        .tags
2080                        .iter()
2081                        .map(|tag| {
2082                            let ty = format!(
2083                                "Client{}Ext::{}",
2084                                sanitize(tag, Case::Pascal),
2085                                method.operation_id,
2086                            );
2087                            format!("[`{}`]: super::{}", ty, ty)
2088                        })
2089                        .collect::<Vec<_>>()
2090                        .join("\n"),
2091                )
2092            }
2093        };
2094
2095        Ok(quote! {
2096            #[doc = #struct_doc]
2097            #derive
2098            pub struct #struct_ident<'a> {
2099                #client_ident: &'a super::Client,
2100                #( #param_names: #param_types, )*
2101            }
2102
2103            impl<'a> #struct_ident<'a> {
2104                pub fn new(client: &'a super::Client) -> Self {
2105                    Self {
2106                        #client_ident: client,
2107                        #( #param_names: #param_values, )*
2108                    }
2109                }
2110
2111                #( #param_impls )*
2112                #send_impl
2113                #stream_impl
2114            }
2115        })
2116    }
2117
2118    fn builder_helper(&self, method: &OperationMethod) -> BuilderImpl {
2119        let operation_id = format_ident!("{}", method.operation_id);
2120        let struct_name = sanitize(&method.operation_id, Case::Pascal);
2121        let struct_ident = format_ident!("{}", struct_name);
2122
2123        let params = method
2124            .params
2125            .iter()
2126            .map(|param| format!("\n    .{}({})", param.name, param.name))
2127            .collect::<Vec<_>>()
2128            .join("");
2129
2130        let eg = format!(
2131            "\
2132            let response = client.{}(){}
2133    .send()
2134    .await;",
2135            method.operation_id, params,
2136        );
2137
2138        // Note that it would be nice to have a non-ignored example that could
2139        // be validated by doc tests, but in order to use the Client we need
2140        // to import it, and in order to import it we need to know the name of
2141        // the containing crate... which we can't from this context.
2142        let doc = format!("{}```ignore\n{}\n```", make_doc_comment(method), eg);
2143
2144        let sig = quote! {
2145            fn #operation_id(&self) -> builder:: #struct_ident <'_>
2146        };
2147
2148        let body = quote! {
2149            builder:: #struct_ident ::new(self)
2150        };
2151        BuilderImpl { doc, sig, body }
2152    }
2153
2154    /// Generates a pair of TokenStreams.
2155    ///
2156    /// The first includes all the operation code; impl Client for operations
2157    /// with no tags and code of this form for each tag:
2158    ///
2159    /// ```ignore
2160    /// pub trait ClientTagExt {
2161    ///     ...
2162    /// }
2163    ///
2164    /// impl ClientTagExt for Client {
2165    ///     ...
2166    /// }
2167    /// ```
2168    ///
2169    /// The second is the code for the prelude for each tag extension trait:
2170    ///
2171    /// ```ignore
2172    /// pub use super::ClientTagExt;
2173    /// ```
2174    pub(crate) fn builder_tags(
2175        &self,
2176        methods: &[OperationMethod],
2177        tag_info: &BTreeMap<&String, &openapiv3::Tag>,
2178    ) -> (TokenStream, TokenStream) {
2179        let mut base = Vec::new();
2180        let mut ext = BTreeMap::new();
2181
2182        methods.iter().for_each(|method| {
2183            let BuilderImpl { doc, sig, body } = self.builder_helper(method);
2184
2185            if method.tags.is_empty() {
2186                let impl_body = quote! {
2187                    #[doc = #doc]
2188                    pub #sig {
2189                        #body
2190                    }
2191                };
2192                base.push(impl_body);
2193            } else {
2194                let trait_sig = quote! {
2195                    #[doc = #doc]
2196                    #sig;
2197                };
2198
2199                let impl_body = quote! {
2200                    #sig {
2201                        #body
2202                    }
2203                };
2204                method.tags.iter().for_each(|tag| {
2205                    ext.entry(tag.clone())
2206                        .or_insert_with(Vec::new)
2207                        .push((trait_sig.clone(), impl_body.clone()));
2208                });
2209            }
2210        });
2211
2212        let base_impl = (!base.is_empty()).then(|| {
2213            quote! {
2214                impl Client {
2215                    #(#base)*
2216                }
2217            }
2218        });
2219
2220        let (ext_impl, ext_use): (Vec<_>, Vec<_>) = ext
2221            .into_iter()
2222            .map(|(tag, trait_methods)| {
2223                let desc = tag_info
2224                    .get(&tag)
2225                    .and_then(|tag| tag.description.as_ref())
2226                    .map(|d| quote! { #[doc = #d] });
2227                let tr = format_ident!("Client{}Ext", sanitize(&tag, Case::Pascal));
2228                let (trait_methods, trait_impls): (Vec<TokenStream>, Vec<TokenStream>) =
2229                    trait_methods.into_iter().unzip();
2230                (
2231                    quote! {
2232                        #desc
2233                        pub trait #tr {
2234                            #(#trait_methods)*
2235                        }
2236
2237                        impl #tr for Client {
2238                            #(#trait_impls)*
2239                        }
2240                    },
2241                    tr,
2242                )
2243            })
2244            .unzip();
2245
2246        (
2247            quote! {
2248                #base_impl
2249
2250                #(#ext_impl)*
2251            },
2252            quote! {
2253                #(pub use super::#ext_use;)*
2254            },
2255        )
2256    }
2257
2258    pub(crate) fn builder_impl(&self, method: &OperationMethod) -> TokenStream {
2259        let BuilderImpl { doc, sig, body } = self.builder_helper(method);
2260
2261        let impl_body = quote! {
2262            #[doc = #doc]
2263            pub #sig {
2264                #body
2265            }
2266        };
2267
2268        impl_body
2269    }
2270
2271    fn get_body_param(
2272        &mut self,
2273        operation: &openapiv3::Operation,
2274        components: &Option<Components>,
2275    ) -> Result<Option<OperationParameter>> {
2276        let body = match &operation.request_body {
2277            Some(body) => body.item(components)?,
2278            None => return Ok(None),
2279        };
2280
2281        let (content_str, media_type) = match (body.content.first(), body.content.len()) {
2282            (None, _) => return Ok(None),
2283            (Some(first), 1) => first,
2284            (_, n) => todo!(
2285                "more media types than expected for {}: {}",
2286                operation.operation_id.as_ref().unwrap(),
2287                n,
2288            ),
2289        };
2290
2291        let schema = media_type.schema.as_ref().ok_or_else(|| {
2292            Error::UnexpectedFormat("No schema specified for request body".to_string())
2293        })?;
2294
2295        let content_type = BodyContentType::from_str(content_str)?;
2296
2297        let typ = match content_type {
2298            BodyContentType::OctetStream => {
2299                // For an octet stream, we expect a simple, specific schema:
2300                // "schema": {
2301                //     "type": "string",
2302                //     "format": "binary"
2303                // }
2304                match schema.item(components)? {
2305                    openapiv3::Schema {
2306                        schema_data:
2307                            openapiv3::SchemaData {
2308                                nullable: false,
2309                                discriminator: None,
2310                                default: None,
2311                                // Other fields that describe or document the
2312                                // schema are fine.
2313                                ..
2314                            },
2315                        schema_kind:
2316                            openapiv3::SchemaKind::Type(openapiv3::Type::String(
2317                                openapiv3::StringType {
2318                                    format:
2319                                        openapiv3::VariantOrUnknownOrEmpty::Item(
2320                                            openapiv3::StringFormat::Binary,
2321                                        ),
2322                                    pattern: None,
2323                                    enumeration,
2324                                    min_length: None,
2325                                    max_length: None,
2326                                },
2327                            )),
2328                    } if enumeration.is_empty() => Ok(()),
2329                    _ => Err(Error::UnexpectedFormat(format!(
2330                        "invalid schema for application/octet-stream: {:?}",
2331                        schema
2332                    ))),
2333                }?;
2334                OperationParameterType::RawBody
2335            }
2336            BodyContentType::Text(_) => {
2337                // For a plain text body, we expect a simple, specific schema:
2338                // "schema": {
2339                //     "type": "string",
2340                // }
2341                match schema.item(components)? {
2342                    openapiv3::Schema {
2343                        schema_data:
2344                            openapiv3::SchemaData {
2345                                nullable: false,
2346                                discriminator: None,
2347                                default: None,
2348                                // Other fields that describe or document the
2349                                // schema are fine.
2350                                ..
2351                            },
2352                        schema_kind:
2353                            openapiv3::SchemaKind::Type(openapiv3::Type::String(
2354                                openapiv3::StringType {
2355                                    format: openapiv3::VariantOrUnknownOrEmpty::Empty,
2356                                    pattern: None,
2357                                    enumeration,
2358                                    min_length: None,
2359                                    max_length: None,
2360                                },
2361                            )),
2362                    } if enumeration.is_empty() => Ok(()),
2363                    _ => Err(Error::UnexpectedFormat(format!(
2364                        "invalid schema for {}: {:?}",
2365                        content_type, schema
2366                    ))),
2367                }?;
2368                OperationParameterType::RawBody
2369            }
2370            BodyContentType::Json | BodyContentType::FormUrlencoded => {
2371                // TODO it would be legal to have the encoding field set for
2372                // application/x-www-form-urlencoded content, but I'm not sure
2373                // how to interpret the values.
2374                if !media_type.encoding.is_empty() {
2375                    todo!("media type encoding not empty: {:#?}", media_type);
2376                }
2377                let name = sanitize(
2378                    &format!("{}-body", operation.operation_id.as_ref().unwrap(),),
2379                    Case::Pascal,
2380                );
2381                let typ = self
2382                    .type_space
2383                    .add_type_with_name(&schema.to_schema(), Some(name))?;
2384                OperationParameterType::Type(typ)
2385            }
2386        };
2387
2388        Ok(Some(OperationParameter {
2389            name: "body".to_string(),
2390            api_name: "body".to_string(),
2391            description: body.description.clone(),
2392            typ,
2393            kind: OperationParameterKind::Body(content_type),
2394        }))
2395    }
2396}
2397
2398fn make_doc_comment(method: &OperationMethod) -> String {
2399    let mut buf = String::new();
2400
2401    if let Some(summary) = &method.summary {
2402        buf.push_str(summary.trim_end_matches(['.', ',']));
2403        buf.push_str("\n\n");
2404    }
2405    if let Some(description) = &method.description {
2406        buf.push_str(description);
2407        buf.push_str("\n\n");
2408    }
2409
2410    buf.push_str(&format!(
2411        "Sends a `{}` request to `{}`\n\n",
2412        method.method.as_str().to_ascii_uppercase(),
2413        method.path.to_string(),
2414    ));
2415
2416    if method
2417        .params
2418        .iter()
2419        .filter(|param| param.description.is_some())
2420        .count()
2421        > 0
2422    {
2423        buf.push_str("Arguments:\n");
2424        for param in &method.params {
2425            buf.push_str(&format!("- `{}`", param.name));
2426            if let Some(description) = &param.description {
2427                buf.push_str(": ");
2428                buf.push_str(description);
2429            }
2430            buf.push('\n');
2431        }
2432    }
2433
2434    buf
2435}
2436
2437fn make_stream_doc_comment(method: &OperationMethod) -> String {
2438    let mut buf = String::new();
2439
2440    if let Some(summary) = &method.summary {
2441        buf.push_str(summary.trim_end_matches(['.', ',']));
2442        buf.push_str(" as a Stream\n\n");
2443    }
2444    if let Some(description) = &method.description {
2445        buf.push_str(description);
2446        buf.push_str("\n\n");
2447    }
2448
2449    buf.push_str(&format!(
2450        "Sends repeated `{}` requests to `{}` until there are no more results.\n\n",
2451        method.method.as_str().to_ascii_uppercase(),
2452        method.path.to_string(),
2453    ));
2454
2455    if method
2456        .params
2457        .iter()
2458        .filter(|param| param.api_name.as_str() != "page_token")
2459        .filter(|param| param.description.is_some())
2460        .count()
2461        > 0
2462    {
2463        buf.push_str("Arguments:\n");
2464        for param in &method.params {
2465            if param.api_name.as_str() == "page_token" {
2466                continue;
2467            }
2468
2469            buf.push_str(&format!("- `{}`", param.name));
2470            if let Some(description) = &param.description {
2471                buf.push_str(": ");
2472                buf.push_str(description);
2473            }
2474            buf.push('\n');
2475        }
2476    }
2477
2478    buf
2479}
2480
2481fn sort_params(raw_params: &mut [OperationParameter], names: &[String]) {
2482    raw_params.sort_by(
2483        |OperationParameter {
2484             kind: a_kind,
2485             api_name: a_name,
2486             ..
2487         },
2488         OperationParameter {
2489             kind: b_kind,
2490             api_name: b_name,
2491             ..
2492         }| {
2493            match (a_kind, b_kind) {
2494                // Path params are first and are in positional order.
2495                (OperationParameterKind::Path, OperationParameterKind::Path) => {
2496                    let a_index = names
2497                        .iter()
2498                        .position(|x| x == a_name)
2499                        .unwrap_or_else(|| panic!("{} missing from path", a_name));
2500                    let b_index = names
2501                        .iter()
2502                        .position(|x| x == b_name)
2503                        .unwrap_or_else(|| panic!("{} missing from path", b_name));
2504                    a_index.cmp(&b_index)
2505                }
2506                (OperationParameterKind::Path, OperationParameterKind::Query(_)) => Ordering::Less,
2507                (OperationParameterKind::Path, OperationParameterKind::Body(_)) => Ordering::Less,
2508                (OperationParameterKind::Path, OperationParameterKind::Header(_)) => Ordering::Less,
2509
2510                // Query params are in lexicographic order.
2511                (OperationParameterKind::Query(_), OperationParameterKind::Body(_)) => {
2512                    Ordering::Less
2513                }
2514                (OperationParameterKind::Query(_), OperationParameterKind::Query(_)) => {
2515                    a_name.cmp(b_name)
2516                }
2517                (OperationParameterKind::Query(_), OperationParameterKind::Path) => {
2518                    Ordering::Greater
2519                }
2520                (OperationParameterKind::Query(_), OperationParameterKind::Header(_)) => {
2521                    Ordering::Less
2522                }
2523
2524                // Body params are last and should be singular.
2525                (OperationParameterKind::Body(_), OperationParameterKind::Path) => {
2526                    Ordering::Greater
2527                }
2528                (OperationParameterKind::Body(_), OperationParameterKind::Query(_)) => {
2529                    Ordering::Greater
2530                }
2531                (OperationParameterKind::Body(_), OperationParameterKind::Header(_)) => {
2532                    Ordering::Greater
2533                }
2534                (OperationParameterKind::Body(_), OperationParameterKind::Body(_)) => {
2535                    panic!("should only be one body")
2536                }
2537
2538                // Header params are in lexicographic order.
2539                (OperationParameterKind::Header(_), OperationParameterKind::Header(_)) => {
2540                    a_name.cmp(b_name)
2541                }
2542                (OperationParameterKind::Header(_), _) => Ordering::Greater,
2543            }
2544        },
2545    );
2546}
2547
2548trait ParameterDataExt {
2549    fn schema(&self) -> Result<&openapiv3::ReferenceOr<openapiv3::Schema>>;
2550}
2551
2552impl ParameterDataExt for openapiv3::ParameterData {
2553    fn schema(&self) -> Result<&openapiv3::ReferenceOr<openapiv3::Schema>> {
2554        match &self.format {
2555            openapiv3::ParameterSchemaOrContent::Schema(s) => Ok(s),
2556            openapiv3::ParameterSchemaOrContent::Content(c) => Err(Error::UnexpectedFormat(
2557                format!("unexpected content {:#?}", c),
2558            )),
2559        }
2560    }
2561}