ploidy_codegen_rust/
operation.rs

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