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