1use indexmap::IndexMap;
2use utoipa::openapi::Required;
3use utoipa::openapi::path::{Parameter, ParameterIn};
4
5use super::param::{ParameterValue, ResolvedParamValue};
6use super::{ParamStyle, ParamValue};
7use crate::client::error::ApiClientError;
8use crate::client::openapi::schema::Schemas;
9
10#[derive(Debug, Default, Clone)]
57pub struct CallQuery {
58 params: IndexMap<String, ResolvedParamValue>,
59 pub(in crate::client) schemas: Schemas,
60}
61
62impl CallQuery {
63 pub fn new() -> Self {
74 Self::default()
75 }
76
77 pub fn add_param<T: ParameterValue>(
107 mut self,
108 name: impl Into<String>,
109 param: impl Into<ParamValue<T>>,
110 ) -> Self {
111 let name = name.into();
112 let param = param.into();
113 if let Some(resolved) = param.resolve(|value| self.schemas.add_example::<T>(value)) {
114 self.params.insert(name, resolved);
115 }
116 self
117 }
118
119 pub(in crate::client) fn is_empty(&self) -> bool {
121 self.params.is_empty()
122 }
123
124 pub(in crate::client) fn to_parameters(&self) -> impl Iterator<Item = Parameter> + '_ {
126 self.params.iter().map(|(name, resolved)| {
128 Parameter::builder()
129 .name(name)
130 .parameter_in(ParameterIn::Query)
131 .required(Required::False) .schema(Some(resolved.schema.clone()))
133 .style(resolved.style.into())
134 .build()
135 })
136 }
137
138 pub(in crate::client) fn to_query_string(&self) -> Result<String, ApiClientError> {
140 let mut pairs = Vec::new();
141
142 for (name, resolved) in &self.params {
143 match resolved.style {
144 ParamStyle::Default | ParamStyle::Form => {
145 self.encode_form_style(name, resolved, &mut pairs)?;
146 }
147 ParamStyle::SpaceDelimited | ParamStyle::PipeDelimited | ParamStyle::Simple => {
148 self.encode_delimited_style(name, resolved, &mut pairs)?;
149 }
150 ParamStyle::DeepObject => {
151 self.encode_deep_object_style(name, resolved, &mut pairs)?;
152 }
153 ParamStyle::Label | ParamStyle::Matrix => {
154 return Err(ApiClientError::UnsupportedParameterValue {
155 message: format!(
156 "Parameter style {:?} is not supported for query parameters",
157 resolved.style
158 ),
159 value: resolved.value.clone(),
160 });
161 }
162 }
163 }
164
165 serde_urlencoded::to_string(&pairs).map_err(ApiClientError::from)
166 }
167
168 fn encode_form_style(
170 &self,
171 name: &str,
172 resolved: &ResolvedParamValue,
173 pairs: &mut Vec<(String, String)>,
174 ) -> Result<(), ApiClientError> {
175 match resolved.to_query_values() {
176 Ok(values) => {
177 for value in values {
178 pairs.push((name.to_string(), value));
179 }
180 Ok(())
181 }
182 Err(err) => Err(err),
183 }
184 }
185
186 fn encode_delimited_style(
188 &self,
189 name: &str,
190 resolved: &ResolvedParamValue,
191 pairs: &mut Vec<(String, String)>,
192 ) -> Result<(), ApiClientError> {
193 match resolved.to_string_value() {
194 Ok(value) => {
195 pairs.push((name.to_string(), value));
196 Ok(())
197 }
198 Err(err) => Err(err),
199 }
200 }
201
202 fn encode_deep_object_style(
204 &self,
205 name: &str,
206 resolved: &ResolvedParamValue,
207 pairs: &mut Vec<(String, String)>,
208 ) -> Result<(), ApiClientError> {
209 match &resolved.value {
210 serde_json::Value::Object(obj) => {
211 for (key, value) in obj {
213 let param_name = format!("{name}[{key}]");
214 let param_value = match value {
215 serde_json::Value::String(s) => s.clone(),
216 serde_json::Value::Number(n) => n.to_string(),
217 serde_json::Value::Bool(b) => b.to_string(),
218 serde_json::Value::Null => String::new(),
219 serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
220 return Err(ApiClientError::UnsupportedParameterValue {
221 message:
222 "nested arrays and objects not supported in DeepObject style"
223 .to_string(),
224 value: value.clone(),
225 });
226 }
227 };
228 pairs.push((param_name, param_value));
229 }
230 Ok(())
231 }
232 _ => Err(ApiClientError::UnsupportedParameterValue {
233 message: "DeepObject style requires object values".to_string(),
234 value: resolved.value.clone(),
235 }),
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_call_query_basic_usage() {
246 let query = CallQuery::new();
247
248 assert!(query.is_empty());
249
250 let query = query
251 .add_param("name", ParamValue::new("test"))
252 .add_param("age", ParamValue::new(25));
253
254 assert!(!query.is_empty());
255 }
256
257 #[test]
258 fn test_ergonomic_api_with_direct_values() {
259 let query = CallQuery::new()
261 .add_param("name", "test")
262 .add_param("age", 25)
263 .add_param("active", true)
264 .add_param("tags", vec!["rust", "web"]);
265
266 assert!(!query.is_empty());
267
268 let query_string = query.to_query_string().expect("should serialize");
269 insta::assert_debug_snapshot!(query_string, @r#""name=test&age=25&active=true&tags=rust&tags=web""#);
270 }
271
272 #[test]
273 fn test_mixed_ergonomic_and_explicit_api() {
274 let query = CallQuery::new()
276 .add_param("name", "test") .add_param("limit", 10) .add_param(
279 "tags",
280 ParamValue::with_style(
281 vec!["rust", "web"],
283 ParamStyle::SpaceDelimited,
284 ),
285 );
286
287 let query_string = query.to_query_string().expect("should serialize");
288 insta::assert_debug_snapshot!(query_string, @r#""name=test&limit=10&tags=rust+web""#);
289 }
290
291 #[test]
292 fn test_query_param_as_query_value() {
293 let query = ParamValue::new("hello world");
294 let value = query.as_query_value().expect("should have value");
295
296 insta::assert_debug_snapshot!(value, @r#"String("hello world")"#);
297 }
298
299 #[test]
300 fn test_query_param_with_different_styles() {
301 let default_query = ParamValue::new("test");
302 assert_eq!(default_query.style, ParamStyle::Default);
303
304 let form_query = ParamValue::with_style("test", ParamStyle::Form);
305 assert_eq!(form_query.style, ParamStyle::Form);
306
307 let space_query = ParamValue::with_style("test", ParamStyle::SpaceDelimited);
308 assert_eq!(space_query.style, ParamStyle::SpaceDelimited);
309
310 let pipe_query = ParamValue::with_style("test", ParamStyle::PipeDelimited);
311 assert_eq!(pipe_query.style, ParamStyle::PipeDelimited);
312 }
313
314 #[test]
315 fn test_query_string_serialization_form_style() {
316 let query = CallQuery::new()
317 .add_param("name", ParamValue::new("john"))
318 .add_param("age", ParamValue::new(25));
319
320 let query_string = query.to_query_string().expect("should serialize");
321 insta::assert_debug_snapshot!(query_string, @r#""name=john&age=25""#);
322 }
323
324 #[test]
325 fn test_query_string_serialization_with_arrays() {
326 let query = CallQuery::new().add_param("tags", ParamValue::new(vec!["rust", "web", "api"]));
327
328 let query_string = query.to_query_string().expect("should serialize");
329 insta::assert_debug_snapshot!(query_string, @r#""tags=rust&tags=web&tags=api""#);
330 }
331
332 #[test]
333 fn test_query_string_serialization_space_delimited() {
334 let query = CallQuery::new().add_param(
335 "tags",
336 ParamValue::with_style(vec!["rust", "web", "api"], ParamStyle::SpaceDelimited),
337 );
338
339 let query_string = query.to_query_string().expect("should serialize");
340 insta::assert_debug_snapshot!(query_string, @r#""tags=rust+web+api""#);
341 }
342
343 #[test]
344 fn test_query_string_serialization_pipe_delimited() {
345 let query = CallQuery::new().add_param(
346 "tags",
347 ParamValue::with_style(vec!["rust", "web", "api"], ParamStyle::PipeDelimited),
348 );
349
350 let query_string = query.to_query_string().expect("should serialize");
351 insta::assert_debug_snapshot!(query_string, @r#""tags=rust%7Cweb%7Capi""#);
352 }
353
354 #[test]
355 fn test_empty_query_serialization() {
356 let query = CallQuery::new();
357 let query_string = query.to_query_string().expect("should serialize");
358 insta::assert_debug_snapshot!(query_string, @r#""""#);
359 }
360
361 #[test]
362 fn test_mixed_parameter_types() {
363 let query = CallQuery::new()
364 .add_param("name", ParamValue::new("john"))
365 .add_param("active", ParamValue::new(true))
366 .add_param("scores", ParamValue::new(vec![10, 20, 30]));
367
368 let query_string = query.to_query_string().expect("should serialize");
369 insta::assert_debug_snapshot!(query_string, @r#""name=john&active=true&scores=10&scores=20&scores=30""#);
370 }
371
372 #[test]
373 fn test_object_query_parameter_error() {
374 use serde_json::json;
375
376 let query = CallQuery::new().add_param("config", ParamValue::new(json!({"key": "value"})));
377
378 let result = query.to_query_string();
379 assert!(matches!(
380 result,
381 Err(ApiClientError::UnsupportedParameterValue { .. })
382 ));
383 }
384
385 #[test]
386 fn test_nested_object_in_array_error() {
387 use serde_json::json;
388
389 let query = CallQuery::new().add_param(
390 "items",
391 ParamValue::new(json!(["valid", {"nested": "object"}])),
392 );
393
394 let result = query.to_query_string();
395 assert!(matches!(
396 result,
397 Err(ApiClientError::UnsupportedParameterValue { .. })
398 ));
399 }
400
401 #[test]
402 fn test_to_parameters_generates_correct_openapi_parameters() {
403 let query = CallQuery::new()
404 .add_param("name", ParamValue::new("test"))
405 .add_param(
406 "tags",
407 ParamValue::with_style(vec!["a", "b"], ParamStyle::SpaceDelimited),
408 )
409 .add_param(
410 "limit",
411 ParamValue::with_style(10, ParamStyle::PipeDelimited),
412 );
413
414 let parameters: Vec<_> = query.to_parameters().collect();
415
416 assert_eq!(parameters.len(), 3);
417
418 for param in ¶meters {
420 assert_eq!(param.parameter_in, ParameterIn::Query);
421 assert_eq!(param.required, Required::False);
422 assert!(param.schema.is_some());
423 }
425
426 let param_names: std::collections::HashSet<_> =
428 parameters.iter().map(|p| p.name.as_str()).collect();
429 assert!(param_names.contains("name"));
430 assert!(param_names.contains("tags"));
431 assert!(param_names.contains("limit"));
432 }
433
434 #[test]
435 fn test_comprehensive_query_serialization_snapshot() {
436 let query = CallQuery::new()
438 .add_param("search", ParamValue::new("hello world"))
439 .add_param("active", ParamValue::new(true))
440 .add_param("count", ParamValue::new(42))
441 .add_param("tags", ParamValue::new(vec!["rust", "api", "web"]))
442 .add_param(
443 "categories",
444 ParamValue::with_style(vec!["tech", "programming"], ParamStyle::SpaceDelimited),
445 )
446 .add_param(
447 "ids",
448 ParamValue::with_style(vec![1, 2, 3], ParamStyle::PipeDelimited),
449 );
450
451 let query_string = query
452 .to_query_string()
453 .expect("serialization should succeed");
454 insta::assert_debug_snapshot!(query_string, @r#""search=hello+world&active=true&count=42&tags=rust&tags=api&tags=web&categories=tech+programming&ids=1%7C2%7C3""#);
455 }
456
457 #[test]
458 fn test_query_parameters_openapi_generation_snapshot() {
459 let query = CallQuery::new()
460 .add_param("q", ParamValue::new("search term"))
461 .add_param(
462 "filters",
463 ParamValue::with_style(vec!["active", "verified"], ParamStyle::SpaceDelimited),
464 )
465 .add_param(
466 "sort",
467 ParamValue::with_style(vec!["name", "date"], ParamStyle::PipeDelimited),
468 );
469
470 let parameters: Vec<_> = query.to_parameters().collect();
471 let debug_params: Vec<_> = parameters
472 .iter()
473 .map(|p| {
474 format!(
475 "{}({:?})",
476 p.name,
477 p.style
478 .as_ref()
479 .unwrap_or(&utoipa::openapi::path::ParameterStyle::Form)
480 )
481 })
482 .collect();
483
484 insta::assert_debug_snapshot!(debug_params, @r#"
485 [
486 "q(Form)",
487 "filters(SpaceDelimited)",
488 "sort(PipeDelimited)",
489 ]
490 "#);
491 }
492
493 #[test]
494 fn test_empty_and_null_values_snapshot() {
495 let query = CallQuery::new()
496 .add_param("empty", ParamValue::new(""))
497 .add_param("nullable", ParamValue::new(serde_json::Value::Null));
498
499 let query_string = query
500 .to_query_string()
501 .expect("serialization should succeed");
502 insta::assert_debug_snapshot!(query_string, @r#""empty=&nullable=""#);
503 }
504
505 #[test]
506 fn test_special_characters_encoding_snapshot() {
507 let query = CallQuery::new()
508 .add_param("special", ParamValue::new("hello & goodbye"))
509 .add_param("unicode", ParamValue::new("café résumé"))
510 .add_param("symbols", ParamValue::new("100% guaranteed!"));
511
512 let query_string = query
513 .to_query_string()
514 .expect("serialization should succeed");
515 insta::assert_debug_snapshot!(query_string, @r#""special=hello+%26+goodbye&unicode=caf%C3%A9+r%C3%A9sum%C3%A9&symbols=100%25+guaranteed%21""#);
516 }
517
518 #[test]
519 fn test_deep_object_style_with_object() {
520 use serde_json::json;
521
522 let query = CallQuery::new().add_param(
523 "user",
524 ParamValue::with_style(
525 json!({"name": "john", "age": 30, "active": true}),
526 ParamStyle::DeepObject,
527 ),
528 );
529
530 let query_string = query
531 .to_query_string()
532 .expect("serialization should succeed");
533
534 assert!(query_string.contains("user%5Bname%5D=john"));
536 assert!(query_string.contains("user%5Bage%5D=30"));
537 assert!(query_string.contains("user%5Bactive%5D=true"));
538 }
539
540 #[test]
541 fn test_deep_object_style_with_nested_object_error() {
542 use serde_json::json;
543
544 let query = CallQuery::new().add_param(
545 "user",
546 ParamValue::with_style(
547 json!({"name": "john", "address": {"street": "123 Main St"}}),
548 ParamStyle::DeepObject,
549 ),
550 );
551
552 let result = query.to_query_string();
553 assert!(matches!(
554 result,
555 Err(ApiClientError::UnsupportedParameterValue { .. })
556 ));
557 }
558
559 #[test]
560 fn test_deep_object_style_with_array_error() {
561 let query = CallQuery::new().add_param(
562 "tags",
563 ParamValue::with_style(vec!["rust", "web"], ParamStyle::DeepObject),
564 );
565
566 let result = query.to_query_string();
567 assert!(matches!(
568 result,
569 Err(ApiClientError::UnsupportedParameterValue { .. })
570 ));
571 }
572
573 #[test]
574 fn test_label_and_matrix_styles_error_in_query() {
575 let query =
576 CallQuery::new().add_param("test", ParamValue::with_style("value", ParamStyle::Label));
577
578 let result = query.to_query_string();
579 assert!(matches!(
580 result,
581 Err(ApiClientError::UnsupportedParameterValue { .. })
582 ));
583
584 let query =
585 CallQuery::new().add_param("test", ParamValue::with_style("value", ParamStyle::Matrix));
586
587 let result = query.to_query_string();
588 assert!(matches!(
589 result,
590 Err(ApiClientError::UnsupportedParameterValue { .. })
591 ));
592 }
593}