ploidy_codegen_rust/
operation.rs1use 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
16pub 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 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 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 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 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 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}