1use ploidy_core::ir::{OperationView, ParameterStyle, ParameterView, QueryParameter, View};
2use proc_macro2::TokenStream;
3use quote::{ToTokens, TokenStreamExt, format_ident, quote};
4
5use super::{
6 derives::ExtraDerive,
7 ext::ParameterViewExt,
8 graph::{CodegenGraph, IdentMapping},
9 naming::CodegenIdentUsage,
10 ref_::CodegenRef,
11};
12
13#[derive(Debug)]
20pub struct CodegenQueryParameters<'a> {
21 graph: &'a CodegenGraph<'a>,
22 op: &'a OperationView<'a, 'a>,
23}
24
25impl<'a> CodegenQueryParameters<'a> {
26 #[inline]
28 pub fn new(graph: &'a CodegenGraph<'a>, op: &'a OperationView<'a, 'a>) -> Self {
29 Self { graph, op }
30 }
31}
32
33impl ToTokens for CodegenQueryParameters<'_> {
34 fn to_tokens(&self, tokens: &mut TokenStream) {
35 let query_name = format_ident!(
36 "{}Query",
37 CodegenIdentUsage::Type(self.graph.ident(self.op.id()))
38 );
39
40 let mut extra_derives = vec![];
41
42 if self.op.query().all(|param| param.hashable()) {
45 extra_derives.push(ExtraDerive::Eq);
46 extra_derives.push(ExtraDerive::Hash);
47 }
48
49 if self
53 .op
54 .query()
55 .all(|param| !param.required() || param.defaultable())
56 {
57 extra_derives.push(ExtraDerive::Default);
58 }
59
60 let fields = self.op.query().map(|param| {
61 let ident = self
62 .graph
63 .ident(IdentMapping::Query(self.op.id(), param.name()));
64 let field_name = CodegenIdentUsage::Field(ident);
65 let serde_attr = SerdeQueryFieldAttr::new(field_name, ¶m);
66
67 let ty = if param.optional() {
68 let view = param.ty();
69 let path = CodegenRef::new(self.graph, &view);
70 quote! { ::std::option::Option<#path> }
71 } else {
72 let view = param.ty();
73 let path = CodegenRef::new(self.graph, &view);
74 quote!(#path)
75 };
76
77 quote! {
78 #serde_attr
79 pub #field_name: #ty,
80 }
81 });
82
83 let styles = self
84 .op
85 .query()
86 .filter_map(|param| Some((param.name(), param.style()?)))
87 .map(|(name, style)| {
88 let style = match style {
89 ParameterStyle::DeepObject => {
90 quote!(::ploidy_util::QueryStyle::DeepObject)
91 }
92 ParameterStyle::SpaceDelimited => {
93 quote!(::ploidy_util::QueryStyle::SpaceDelimited)
94 }
95 ParameterStyle::PipeDelimited => {
96 quote!(::ploidy_util::QueryStyle::PipeDelimited)
97 }
98 ParameterStyle::Form { exploded } => {
99 quote!(::ploidy_util::QueryStyle::Form { exploded: #exploded })
100 }
101 };
102 quote!((#name, #style))
103 });
104
105 tokens.append_all(quote! {
106 #[derive(Debug, Clone, PartialEq, #(#extra_derives,)* ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
107 #[serde(crate = "::ploidy_util::serde")]
108 pub struct #query_name {
109 #(#fields)*
110 }
111
112 impl #query_name {
113 pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[#(#styles,)*];
114 }
115 });
116 }
117}
118
119#[derive(Debug)]
121struct SerdeQueryFieldAttr<'param, 'a> {
122 field_name: CodegenIdentUsage<'param>,
123 param: &'param ParameterView<'param, 'a, 'a, QueryParameter>,
124}
125
126impl<'param, 'a> SerdeQueryFieldAttr<'param, 'a> {
127 fn new(
128 field_name: CodegenIdentUsage<'param>,
129 param: &'param ParameterView<'param, 'a, 'a, QueryParameter>,
130 ) -> Self {
131 Self { field_name, param }
132 }
133}
134
135impl ToTokens for SerdeQueryFieldAttr<'_, '_> {
136 fn to_tokens(&self, tokens: &mut TokenStream) {
137 let mut attrs = vec![];
138
139 let param_name = self.param.name();
140 if self.field_name.display().to_string() != param_name {
141 attrs.push(quote! { rename = #param_name });
142 }
143
144 if self.param.optional() {
145 attrs.push(quote! { default });
146 attrs.push(quote! { skip_serializing_if = "Option::is_none" });
147 }
148
149 if !attrs.is_empty() {
150 tokens.append_all(quote! { #[serde(#(#attrs),*)] });
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 use ploidy_core::{
160 arena::Arena,
161 ir::{RawGraph, Spec},
162 parse::Document,
163 };
164 use pretty_assertions::assert_eq;
165 use syn::parse_quote;
166
167 use crate::CodegenGraph;
168
169 #[test]
170 fn test_all_optional_query_params() {
171 let doc = Document::from_yaml(indoc::indoc! {"
172 openapi: 3.0.0
173 info:
174 title: Test API
175 version: 1.0.0
176 paths:
177 /charts/{chart_id}:
178 get:
179 operationId: fetchChart
180 parameters:
181 - name: chart_id
182 in: path
183 required: true
184 schema:
185 type: string
186 - name: refresh
187 in: query
188 schema:
189 type: boolean
190 - name: client_job_id
191 in: query
192 schema:
193 type: string
194 - name: partition_idx
195 in: query
196 schema:
197 type: integer
198 format: int32
199 responses:
200 '200':
201 description: OK
202 "})
203 .unwrap();
204
205 let arena = Arena::new();
206 let spec = Spec::from_doc(&arena, &doc).unwrap();
207 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
208
209 let op = graph.operations().next().unwrap();
210 let codegen = CodegenQueryParameters::new(&graph, &op);
211
212 let actual: syn::File = parse_quote!(#codegen);
213 let expected: syn::File = parse_quote! {
214 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
215 #[serde(crate = "::ploidy_util::serde")]
216 pub struct FetchChartQuery {
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub refresh: ::std::option::Option<bool>,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub client_job_id: ::std::option::Option<::std::string::String>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub partition_idx: ::std::option::Option<i32>,
223 }
224
225 impl FetchChartQuery {
226 pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[];
227 }
228 };
229 assert_eq!(actual, expected);
230 }
231
232 #[test]
233 fn test_required_and_optional_query_params() {
234 let doc = Document::from_yaml(indoc::indoc! {"
235 openapi: 3.0.0
236 info:
237 title: Test API
238 version: 1.0.0
239 paths:
240 /items:
241 get:
242 operationId: listItems
243 parameters:
244 - name: page
245 in: query
246 required: true
247 schema:
248 type: integer
249 format: int32
250 - name: perPage
251 in: query
252 style: pipeDelimited
253 schema:
254 type: array
255 items:
256 type: integer
257 format: int32
258 responses:
259 '200':
260 description: OK
261 "})
262 .unwrap();
263
264 let arena = Arena::new();
265 let spec = Spec::from_doc(&arena, &doc).unwrap();
266 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
267
268 let op = graph.operations().next().unwrap();
269 let codegen = CodegenQueryParameters::new(&graph, &op);
270
271 let actual: syn::File = parse_quote!(#codegen);
272 let expected: syn::File = parse_quote! {
273 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
274 #[serde(crate = "::ploidy_util::serde")]
275 pub struct ListItemsQuery {
276 pub page: i32,
277 #[serde(rename = "perPage", default, skip_serializing_if = "Option::is_none")]
278 pub per_page: ::std::option::Option<::std::vec::Vec<i32>>,
279 }
280
281 impl ListItemsQuery {
282 pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[
283 ("perPage", ::ploidy_util::QueryStyle::PipeDelimited),
284 ];
285 }
286 };
287 assert_eq!(actual, expected);
288 }
289
290 #[test]
291 fn test_excludes_eq_hash_for_float_params() {
292 let doc = Document::from_yaml(indoc::indoc! {"
293 openapi: 3.0.0
294 info:
295 title: Test API
296 version: 1.0.0
297 paths:
298 /items:
299 get:
300 operationId: getItems
301 parameters:
302 - name: threshold
303 in: query
304 schema:
305 type: number
306 format: double
307 responses:
308 '200':
309 description: OK
310 "})
311 .unwrap();
312
313 let arena = Arena::new();
314 let spec = Spec::from_doc(&arena, &doc).unwrap();
315 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
316
317 let op = graph.operations().next().unwrap();
318 let codegen = CodegenQueryParameters::new(&graph, &op);
319
320 let actual: syn::File = parse_quote!(#codegen);
321 let expected: syn::File = parse_quote! {
322 #[derive(Debug, Clone, PartialEq, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
323 #[serde(crate = "::ploidy_util::serde")]
324 pub struct GetItemsQuery {
325 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub threshold: ::std::option::Option<f64>,
327 }
328
329 impl GetItemsQuery {
330 pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[];
331 }
332 };
333 assert_eq!(actual, expected);
334 }
335
336 #[test]
337 fn test_excludes_default_for_non_defaultable_required_param() {
338 let doc = Document::from_yaml(indoc::indoc! {"
339 openapi: 3.0.0
340 info:
341 title: Test API
342 version: 1.0.0
343 paths:
344 /items:
345 get:
346 operationId: getItems
347 parameters:
348 - name: callback
349 in: query
350 required: true
351 schema:
352 type: string
353 format: uri
354 responses:
355 '200':
356 description: OK
357 "})
358 .unwrap();
359
360 let arena = Arena::new();
361 let spec = Spec::from_doc(&arena, &doc).unwrap();
362 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
363
364 let op = graph.operations().next().unwrap();
365 let codegen = CodegenQueryParameters::new(&graph, &op);
366
367 let actual: syn::File = parse_quote!(#codegen);
368 let expected: syn::File = parse_quote! {
369 #[derive(Debug, Clone, PartialEq, Eq, Hash, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
370 #[serde(crate = "::ploidy_util::serde")]
371 pub struct GetItemsQuery {
372 pub callback: ::ploidy_util::url::Url,
373 }
374
375 impl GetItemsQuery {
376 pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[];
377 }
378 };
379 assert_eq!(actual, expected);
380 }
381
382 #[test]
383 fn test_query_parameter_styles() {
384 let doc = Document::from_yaml(indoc::indoc! {"
385 openapi: 3.0.0
386 info:
387 title: Test API
388 version: 1.0.0
389 paths:
390 /items:
391 get:
392 operationId: listItems
393 parameters:
394 - name: filter
395 in: query
396 style: deepObject
397 schema:
398 type: object
399 properties:
400 status:
401 type: string
402 - name: tags
403 in: query
404 style: pipeDelimited
405 schema:
406 type: array
407 items:
408 type: string
409 - name: ids
410 in: query
411 style: spaceDelimited
412 schema:
413 type: array
414 items:
415 type: integer
416 format: int32
417 - name: colors
418 in: query
419 style: form
420 explode: false
421 schema:
422 type: array
423 items:
424 type: string
425 responses:
426 '200':
427 description: OK
428 "})
429 .unwrap();
430
431 let arena = Arena::new();
432 let spec = Spec::from_doc(&arena, &doc).unwrap();
433 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
434
435 let op = graph.operations().next().unwrap();
436 let codegen = CodegenQueryParameters::new(&graph, &op);
437
438 let actual: syn::File = parse_quote!(#codegen);
439 let expected: syn::File = parse_quote! {
440 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
441 #[serde(crate = "::ploidy_util::serde")]
442 pub struct ListItemsQuery {
443 #[serde(default, skip_serializing_if = "Option::is_none")]
444 pub filter: ::std::option::Option<crate::client::default::types::ListItemsQueryFilter>,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
446 pub tags: ::std::option::Option<::std::vec::Vec<::std::string::String>>,
447 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub ids: ::std::option::Option<::std::vec::Vec<i32>>,
449 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub colors: ::std::option::Option<::std::vec::Vec<::std::string::String>>,
451 }
452
453 impl ListItemsQuery {
454 pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[
455 ("filter", ::ploidy_util::QueryStyle::DeepObject),
456 ("tags", ::ploidy_util::QueryStyle::PipeDelimited),
457 ("ids", ::ploidy_util::QueryStyle::SpaceDelimited),
458 ("colors", ::ploidy_util::QueryStyle::Form { exploded: false }),
459 ];
460 }
461 };
462 assert_eq!(actual, expected);
463 }
464
465 #[test]
466 fn test_ref_query_parameter() {
467 let doc = Document::from_yaml(indoc::indoc! {"
468 openapi: 3.0.0
469 info:
470 title: Test API
471 version: 1.0.0
472 paths:
473 /items:
474 get:
475 operationId: listItems
476 parameters:
477 - name: sort
478 in: query
479 schema:
480 $ref: '#/components/schemas/SortOrder'
481 responses:
482 '200':
483 description: OK
484 components:
485 schemas:
486 SortOrder:
487 type: string
488 enum:
489 - asc
490 - desc
491 "})
492 .unwrap();
493
494 let arena = Arena::new();
495 let spec = Spec::from_doc(&arena, &doc).unwrap();
496 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
497
498 let op = graph.operations().next().unwrap();
499 let codegen = CodegenQueryParameters::new(&graph, &op);
500
501 let actual: syn::File = parse_quote!(#codegen);
502 let expected: syn::File = parse_quote! {
503 #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, ::ploidy_util::serde::Serialize, ::ploidy_util::serde::Deserialize)]
504 #[serde(crate = "::ploidy_util::serde")]
505 pub struct ListItemsQuery {
506 #[serde(default, skip_serializing_if = "Option::is_none")]
507 pub sort: ::std::option::Option<crate::types::SortOrder>,
508 }
509
510 impl ListItemsQuery {
511 pub const STYLES: &[(&str, ::ploidy_util::QueryStyle)] = &[];
512 }
513 };
514 assert_eq!(actual, expected);
515 }
516}