1use indexmap::IndexMap;
2use utoipa::openapi::Required;
3use utoipa::openapi::path::{Parameter, ParameterIn};
4
5use super::param::ParameterValue;
6use super::param::ResolvedParamValue;
7use super::schema::Schemas;
8use super::{ApiClientError, ParamStyle, ParamValue};
9
10#[derive(Debug, Default, Clone)]
57pub struct CallQuery {
58 params: IndexMap<String, ResolvedParamValue>,
59 pub(super) 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(super) fn is_empty(&self) -> bool {
121 self.params.is_empty()
122 }
123
124 pub(super) 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(super) 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 }
151 }
152
153 serde_urlencoded::to_string(&pairs).map_err(ApiClientError::from)
154 }
155
156 fn encode_form_style(
158 &self,
159 name: &str,
160 resolved: &ResolvedParamValue,
161 pairs: &mut Vec<(String, String)>,
162 ) -> Result<(), ApiClientError> {
163 match resolved.to_query_values() {
164 Ok(values) => {
165 for value in values {
166 pairs.push((name.to_string(), value));
167 }
168 Ok(())
169 }
170 Err(err) => Err(err),
171 }
172 }
173
174 fn encode_delimited_style(
176 &self,
177 name: &str,
178 resolved: &ResolvedParamValue,
179 pairs: &mut Vec<(String, String)>,
180 ) -> Result<(), ApiClientError> {
181 match resolved.to_string_value() {
182 Ok(value) => {
183 pairs.push((name.to_string(), value));
184 Ok(())
185 }
186 Err(err) => Err(err),
187 }
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_call_query_basic_usage() {
197 let query = CallQuery::new();
198
199 assert!(query.is_empty());
200
201 let query = query
202 .add_param("name", ParamValue::new("test"))
203 .add_param("age", ParamValue::new(25));
204
205 assert!(!query.is_empty());
206 }
207
208 #[test]
209 fn test_ergonomic_api_with_direct_values() {
210 let query = CallQuery::new()
212 .add_param("name", "test")
213 .add_param("age", 25)
214 .add_param("active", true)
215 .add_param("tags", vec!["rust", "web"]);
216
217 assert!(!query.is_empty());
218
219 let query_string = query.to_query_string().expect("should serialize");
220 insta::assert_debug_snapshot!(query_string, @r#""name=test&age=25&active=true&tags=rust&tags=web""#);
221 }
222
223 #[test]
224 fn test_mixed_ergonomic_and_explicit_api() {
225 let query = CallQuery::new()
227 .add_param("name", "test") .add_param("limit", 10) .add_param(
230 "tags",
231 ParamValue::with_style(
232 vec!["rust", "web"],
234 ParamStyle::SpaceDelimited,
235 ),
236 );
237
238 let query_string = query.to_query_string().expect("should serialize");
239 insta::assert_debug_snapshot!(query_string, @r#""name=test&limit=10&tags=rust+web""#);
240 }
241
242 #[test]
243 fn test_query_param_as_query_value() {
244 let query = ParamValue::new("hello world");
245 let value = query.as_query_value().expect("should have value");
246
247 insta::assert_debug_snapshot!(value, @r#"String("hello world")"#);
248 }
249
250 #[test]
251 fn test_query_param_with_different_styles() {
252 let default_query = ParamValue::new("test");
253 assert_eq!(default_query.style, ParamStyle::Default);
254
255 let form_query = ParamValue::with_style("test", ParamStyle::Form);
256 assert_eq!(form_query.style, ParamStyle::Form);
257
258 let space_query = ParamValue::with_style("test", ParamStyle::SpaceDelimited);
259 assert_eq!(space_query.style, ParamStyle::SpaceDelimited);
260
261 let pipe_query = ParamValue::with_style("test", ParamStyle::PipeDelimited);
262 assert_eq!(pipe_query.style, ParamStyle::PipeDelimited);
263 }
264
265 #[test]
266 fn test_query_string_serialization_form_style() {
267 let query = CallQuery::new()
268 .add_param("name", ParamValue::new("john"))
269 .add_param("age", ParamValue::new(25));
270
271 let query_string = query.to_query_string().expect("should serialize");
272 insta::assert_debug_snapshot!(query_string, @r#""name=john&age=25""#);
273 }
274
275 #[test]
276 fn test_query_string_serialization_with_arrays() {
277 let query = CallQuery::new().add_param("tags", ParamValue::new(vec!["rust", "web", "api"]));
278
279 let query_string = query.to_query_string().expect("should serialize");
280 insta::assert_debug_snapshot!(query_string, @r#""tags=rust&tags=web&tags=api""#);
281 }
282
283 #[test]
284 fn test_query_string_serialization_space_delimited() {
285 let query = CallQuery::new().add_param(
286 "tags",
287 ParamValue::with_style(vec!["rust", "web", "api"], ParamStyle::SpaceDelimited),
288 );
289
290 let query_string = query.to_query_string().expect("should serialize");
291 insta::assert_debug_snapshot!(query_string, @r#""tags=rust+web+api""#);
292 }
293
294 #[test]
295 fn test_query_string_serialization_pipe_delimited() {
296 let query = CallQuery::new().add_param(
297 "tags",
298 ParamValue::with_style(vec!["rust", "web", "api"], ParamStyle::PipeDelimited),
299 );
300
301 let query_string = query.to_query_string().expect("should serialize");
302 insta::assert_debug_snapshot!(query_string, @r#""tags=rust%7Cweb%7Capi""#);
303 }
304
305 #[test]
306 fn test_empty_query_serialization() {
307 let query = CallQuery::new();
308 let query_string = query.to_query_string().expect("should serialize");
309 insta::assert_debug_snapshot!(query_string, @r#""""#);
310 }
311
312 #[test]
313 fn test_mixed_parameter_types() {
314 let query = CallQuery::new()
315 .add_param("name", ParamValue::new("john"))
316 .add_param("active", ParamValue::new(true))
317 .add_param("scores", ParamValue::new(vec![10, 20, 30]));
318
319 let query_string = query.to_query_string().expect("should serialize");
320 insta::assert_debug_snapshot!(query_string, @r#""name=john&active=true&scores=10&scores=20&scores=30""#);
321 }
322
323 #[test]
324 fn test_object_query_parameter_error() {
325 use serde_json::json;
326
327 let query = CallQuery::new().add_param("config", ParamValue::new(json!({"key": "value"})));
328
329 let result = query.to_query_string();
330 assert!(matches!(
331 result,
332 Err(ApiClientError::UnsupportedParameterValue { .. })
333 ));
334 }
335
336 #[test]
337 fn test_nested_object_in_array_error() {
338 use serde_json::json;
339
340 let query = CallQuery::new().add_param(
341 "items",
342 ParamValue::new(json!(["valid", {"nested": "object"}])),
343 );
344
345 let result = query.to_query_string();
346 assert!(matches!(
347 result,
348 Err(ApiClientError::UnsupportedParameterValue { .. })
349 ));
350 }
351
352 #[test]
353 fn test_to_parameters_generates_correct_openapi_parameters() {
354 let query = CallQuery::new()
355 .add_param("name", ParamValue::new("test"))
356 .add_param(
357 "tags",
358 ParamValue::with_style(vec!["a", "b"], ParamStyle::SpaceDelimited),
359 )
360 .add_param(
361 "limit",
362 ParamValue::with_style(10, ParamStyle::PipeDelimited),
363 );
364
365 let parameters: Vec<_> = query.to_parameters().collect();
366
367 assert_eq!(parameters.len(), 3);
368
369 for param in ¶meters {
371 assert_eq!(param.parameter_in, ParameterIn::Query);
372 assert_eq!(param.required, Required::False);
373 assert!(param.schema.is_some());
374 }
376
377 let param_names: std::collections::HashSet<_> =
379 parameters.iter().map(|p| p.name.as_str()).collect();
380 assert!(param_names.contains("name"));
381 assert!(param_names.contains("tags"));
382 assert!(param_names.contains("limit"));
383 }
384
385 #[test]
386 fn test_comprehensive_query_serialization_snapshot() {
387 let query = CallQuery::new()
389 .add_param("search", ParamValue::new("hello world"))
390 .add_param("active", ParamValue::new(true))
391 .add_param("count", ParamValue::new(42))
392 .add_param("tags", ParamValue::new(vec!["rust", "api", "web"]))
393 .add_param(
394 "categories",
395 ParamValue::with_style(vec!["tech", "programming"], ParamStyle::SpaceDelimited),
396 )
397 .add_param(
398 "ids",
399 ParamValue::with_style(vec![1, 2, 3], ParamStyle::PipeDelimited),
400 );
401
402 let query_string = query
403 .to_query_string()
404 .expect("serialization should succeed");
405 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""#);
406 }
407
408 #[test]
409 fn test_query_parameters_openapi_generation_snapshot() {
410 let query = CallQuery::new()
411 .add_param("q", ParamValue::new("search term"))
412 .add_param(
413 "filters",
414 ParamValue::with_style(vec!["active", "verified"], ParamStyle::SpaceDelimited),
415 )
416 .add_param(
417 "sort",
418 ParamValue::with_style(vec!["name", "date"], ParamStyle::PipeDelimited),
419 );
420
421 let parameters: Vec<_> = query.to_parameters().collect();
422 let debug_params: Vec<_> = parameters
423 .iter()
424 .map(|p| {
425 format!(
426 "{}({:?})",
427 p.name,
428 p.style
429 .as_ref()
430 .unwrap_or(&utoipa::openapi::path::ParameterStyle::Form)
431 )
432 })
433 .collect();
434
435 insta::assert_debug_snapshot!(debug_params, @r#"
436 [
437 "q(Form)",
438 "filters(SpaceDelimited)",
439 "sort(PipeDelimited)",
440 ]
441 "#);
442 }
443
444 #[test]
445 fn test_empty_and_null_values_snapshot() {
446 let query = CallQuery::new()
447 .add_param("empty", ParamValue::new(""))
448 .add_param("nullable", ParamValue::new(serde_json::Value::Null));
449
450 let query_string = query
451 .to_query_string()
452 .expect("serialization should succeed");
453 insta::assert_debug_snapshot!(query_string, @r#""empty=&nullable=""#);
454 }
455
456 #[test]
457 fn test_special_characters_encoding_snapshot() {
458 let query = CallQuery::new()
459 .add_param("special", ParamValue::new("hello & goodbye"))
460 .add_param("unicode", ParamValue::new("café résumé"))
461 .add_param("symbols", ParamValue::new("100% guaranteed!"));
462
463 let query_string = query
464 .to_query_string()
465 .expect("serialization should succeed");
466 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""#);
467 }
468}