ploidy_codegen_rust/
operation.rs

1use itertools::Itertools;
2use ploidy_core::{
3    codegen::UniqueNames,
4    ir::{
5        IrOperationView, IrParameterStyle, IrParameterView, IrPathParameter, IrQueryParameter,
6        IrRequestView, IrResponseView, IrTypeView,
7    },
8    parse::{Method, path::PathFragment},
9};
10use proc_macro2::{Span, TokenStream};
11use quote::{ToTokens, TokenStreamExt, quote};
12use syn::Ident;
13
14use super::{
15    doc_attrs,
16    naming::{CodegenIdent, CodegenIdentScope, CodegenIdentUsage},
17    ref_::CodegenRef,
18};
19
20/// Generates a single client method for an API operation.
21pub struct CodegenOperation<'a> {
22    op: &'a IrOperationView<'a>,
23}
24
25impl<'a> CodegenOperation<'a> {
26    pub fn new(op: &'a IrOperationView<'a>) -> Self {
27        Self { op }
28    }
29
30    /// Generates code to build the request URL, with path parameters substituted.
31    fn url(&self, params: &[(CodegenIdent, IrParameterView<'_, IrPathParameter>)]) -> TokenStream {
32        let segments = self
33            .op
34            .path()
35            .segments()
36            .map(|segment| match segment.fragments() {
37                [] => quote! { "" },
38                [PathFragment::Literal(text)] => quote! { #text },
39                [PathFragment::Param(name)] => {
40                    let (ident, _) = params
41                        .iter()
42                        .find(|(_, param)| param.name() == *name)
43                        .unwrap();
44                    let usage = CodegenIdentUsage::Param(ident);
45                    quote!(#usage)
46                }
47                fragments => {
48                    // Build a format string, with placeholders for parameter fragments.
49                    let format = fragments.iter().fold(String::new(), |mut f, fragment| {
50                        match fragment {
51                            PathFragment::Literal(text) => {
52                                f.push_str(&text.replace('{', "{{").replace('}', "}}"))
53                            }
54                            PathFragment::Param(_) => f.push_str("{}"),
55                        }
56                        f
57                    });
58                    let args = fragments
59                        .iter()
60                        .filter_map(|fragment| match fragment {
61                            PathFragment::Param(name) => Some(name),
62                            PathFragment::Literal(_) => None,
63                        })
64                        .map(|name| {
65                            // `url::PathSegmentsMut::push` percent-encodes the
66                            // full segment, so we can interpolate fragments
67                            // directly.
68                            let (ident, _) = params
69                                .iter()
70                                .find(|(_, param)| param.name() == *name)
71                                .unwrap();
72                            CodegenIdentUsage::Param(ident)
73                        });
74                    quote! { &format!(#format, #(#args),*) }
75                }
76            });
77        quote! {
78            let url = {
79                let mut url = self.base_url.clone();
80                url
81                    .path_segments_mut()
82                    .map_err(|()| crate::error::Error::UrlCannotBeABase)?
83                    .pop_if_empty()
84                    #(.push(#segments))*;
85                url
86            };
87        }
88    }
89
90    /// Generates code to append query parameters.
91    fn query(
92        &self,
93        params: &[(CodegenIdent, IrParameterView<'_, IrQueryParameter>)],
94    ) -> TokenStream {
95        let appends = params
96            .iter()
97            .map(|(ident, param)| {
98                let name = param.name();
99                let style = match param.style() {
100                    Some(IrParameterStyle::DeepObject) => {
101                        quote!(::ploidy_util::QueryStyle::DeepObject)
102                    }
103                    Some(IrParameterStyle::SpaceDelimited) => {
104                        quote!(::ploidy_util::QueryStyle::SpaceDelimited)
105                    }
106                    Some(IrParameterStyle::PipeDelimited) => {
107                        quote!(::ploidy_util::QueryStyle::PipeDelimited)
108                    }
109                    Some(IrParameterStyle::Form { exploded }) => {
110                        quote!(::ploidy_util::QueryStyle::Form { exploded: #exploded })
111                    }
112                    None => quote!(::ploidy_util::QueryStyle::default()),
113                };
114                let usage = CodegenIdentUsage::Param(ident);
115                Some(quote! {
116                    .style(#style)
117                    .append(#name, &#usage)?
118                })
119            })
120            .collect_vec();
121        match &*appends {
122            [] => quote! {},
123            appends => quote! {
124                let url = {
125                    let mut url = url;
126                    let serializer = ::ploidy_util::QuerySerializer::new(&mut url);
127                    serializer #(#appends)*;
128                    url
129                };
130            },
131        }
132    }
133}
134
135impl ToTokens for CodegenOperation<'_> {
136    fn to_tokens(&self, tokens: &mut TokenStream) {
137        let operation_id = CodegenIdent::new(self.op.id());
138        let method_name = CodegenIdentUsage::Method(&operation_id);
139
140        let unique = UniqueNames::new();
141        let mut scope = CodegenIdentScope::with_reserved(&unique, &["url", "request", "form"]);
142        let mut params = vec![];
143
144        let paths = self
145            .op
146            .path()
147            .params()
148            .map(|param| (scope.uniquify(param.name()), param))
149            .collect_vec();
150        for (ident, _) in &paths {
151            let usage = CodegenIdentUsage::Param(ident);
152            params.push(quote! { #usage: &str });
153        }
154
155        let queries = self
156            .op
157            .query()
158            .map(|param| (scope.uniquify(param.name()), param))
159            .collect_vec();
160        for (ident, param) in &queries {
161            let view = param.ty();
162            let ty = if param.required() || matches!(view, IrTypeView::Optional(_)) {
163                let path = CodegenRef::new(&view);
164                quote!(#path)
165            } else {
166                let path = CodegenRef::new(&view);
167                quote! { ::std::option::Option<#path> }
168            };
169            let usage = CodegenIdentUsage::Param(ident);
170            params.push(quote! { #usage: #ty });
171        }
172
173        if let Some(request) = self.op.request() {
174            match request {
175                IrRequestView::Json(view) => {
176                    let param_type = CodegenRef::new(&view);
177                    params.push(quote! { request: impl Into<#param_type> });
178                }
179                IrRequestView::Multipart => {
180                    params.push(quote! { form: reqwest::multipart::Form });
181                }
182            }
183        }
184
185        let return_type = match self.op.response() {
186            Some(response) => match response {
187                IrResponseView::Json(view) => CodegenRef::new(&view).into_token_stream(),
188            },
189            None => quote! { () },
190        };
191
192        let build_url = self.url(&paths);
193        let build_query = self.query(&queries);
194
195        let http_method = CodegenMethod(self.op.method());
196        let build_request = match self.op.request() {
197            Some(IrRequestView::Json(_)) => quote! {
198                let response = self.client
199                    .#http_method(url)
200                    .headers(self.headers.clone())
201                    .json(&request.into())
202                    .send()
203                    .await?
204                    .error_for_status()?;
205            },
206            Some(IrRequestView::Multipart) => quote! {
207                let response = self.client
208                    .#http_method(url)
209                    .headers(self.headers.clone())
210                    .multipart(form)
211                    .send()
212                    .await?
213                    .error_for_status()?;
214            },
215            None => quote! {
216                let response = self.client
217                    .#http_method(url)
218                    .headers(self.headers.clone())
219                    .send()
220                    .await?
221                    .error_for_status()?;
222            },
223        };
224
225        let parse_response = if self.op.response().is_some() {
226            quote! {
227                let body = response.bytes().await?;
228                let deserializer = &mut serde_json::Deserializer::from_slice(&body);
229                let result = serde_path_to_error::deserialize(deserializer)
230                    .map_err(crate::error::JsonError::from)?;
231                Ok(result)
232            }
233        } else {
234            quote! {
235                let _ = response;
236                Ok(())
237            }
238        };
239
240        let doc = self.op.description().map(doc_attrs);
241
242        tokens.append_all(quote! {
243            #doc
244            pub async fn #method_name(
245                &self,
246                #(#params),*
247            ) -> Result<#return_type, crate::error::Error> {
248                #build_url
249                #build_query
250                #build_request
251                #parse_response
252            }
253        });
254    }
255}
256
257#[derive(Clone, Copy, Debug)]
258pub struct CodegenMethod(pub Method);
259
260impl ToTokens for CodegenMethod {
261    fn to_tokens(&self, tokens: &mut TokenStream) {
262        tokens.append(match self.0 {
263            Method::Get => Ident::new("get", Span::call_site()),
264            Method::Post => Ident::new("post", Span::call_site()),
265            Method::Put => Ident::new("put", Span::call_site()),
266            Method::Patch => Ident::new("patch", Span::call_site()),
267            Method::Delete => Ident::new("delete", Span::call_site()),
268        });
269    }
270}