use indexmap::IndexMap;
use utoipa::openapi::Required;
use utoipa::openapi::path::{Parameter, ParameterIn};
use super::param::{ParameterValue, ResolvedParamValue};
use super::{ParamStyle, ParamValue};
use crate::client::error::ApiClientError;
use crate::client::openapi::schema::Schemas;
#[derive(Debug, Default, Clone)]
pub struct CallQuery {
params: IndexMap<String, ResolvedParamValue>,
pub(in crate::client) schemas: Schemas,
}
impl CallQuery {
pub fn new() -> Self {
Self::default()
}
pub fn add_param<T: ParameterValue>(
mut self,
name: impl Into<String>,
param: impl Into<ParamValue<T>>,
) -> Self {
let name = name.into();
let param = param.into();
if let Some(resolved) = param.resolve(|value| self.schemas.add_example::<T>(value)) {
self.params.insert(name, resolved);
}
self
}
pub(in crate::client) fn is_empty(&self) -> bool {
self.params.is_empty()
}
pub(in crate::client) fn to_parameters(&self) -> impl Iterator<Item = Parameter> + '_ {
self.params.iter().map(|(name, resolved)| {
Parameter::builder()
.name(name)
.parameter_in(ParameterIn::Query)
.required(Required::False) .schema(Some(resolved.schema.clone()))
.style(resolved.style.into())
.build()
})
}
pub(in crate::client) fn to_query_string(&self) -> Result<String, ApiClientError> {
let mut pairs = Vec::new();
for (name, resolved) in &self.params {
match resolved.style {
ParamStyle::Default | ParamStyle::Form => {
self.encode_form_style(name, resolved, &mut pairs)?;
}
ParamStyle::SpaceDelimited | ParamStyle::PipeDelimited | ParamStyle::Simple => {
self.encode_delimited_style(name, resolved, &mut pairs)?;
}
ParamStyle::DeepObject => {
self.encode_deep_object_style(name, resolved, &mut pairs)?;
}
ParamStyle::Label | ParamStyle::Matrix => {
return Err(ApiClientError::UnsupportedParameterValue {
message: format!(
"Parameter style {:?} is not supported for query parameters",
resolved.style
),
value: resolved.value.clone(),
});
}
}
}
serde_urlencoded::to_string(&pairs).map_err(ApiClientError::from)
}
fn encode_form_style(
&self,
name: &str,
resolved: &ResolvedParamValue,
pairs: &mut Vec<(String, String)>,
) -> Result<(), ApiClientError> {
match resolved.to_query_values() {
Ok(values) => {
for value in values {
pairs.push((name.to_string(), value));
}
Ok(())
}
Err(err) => Err(err),
}
}
fn encode_delimited_style(
&self,
name: &str,
resolved: &ResolvedParamValue,
pairs: &mut Vec<(String, String)>,
) -> Result<(), ApiClientError> {
match resolved.to_string_value() {
Ok(value) => {
pairs.push((name.to_string(), value));
Ok(())
}
Err(err) => Err(err),
}
}
fn encode_deep_object_style(
&self,
name: &str,
resolved: &ResolvedParamValue,
pairs: &mut Vec<(String, String)>,
) -> Result<(), ApiClientError> {
match &resolved.value {
serde_json::Value::Object(obj) => {
for (key, value) in obj {
let param_name = format!("{name}[{key}]");
let param_value = match value {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Null => String::new(),
serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
return Err(ApiClientError::UnsupportedParameterValue {
message:
"nested arrays and objects not supported in DeepObject style"
.to_string(),
value: value.clone(),
});
}
};
pairs.push((param_name, param_value));
}
Ok(())
}
_ => Err(ApiClientError::UnsupportedParameterValue {
message: "DeepObject style requires object values".to_string(),
value: resolved.value.clone(),
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_call_query_basic_usage() {
let query = CallQuery::new();
assert!(query.is_empty());
let query = query
.add_param("name", ParamValue::new("test"))
.add_param("age", ParamValue::new(25));
assert!(!query.is_empty());
}
#[test]
fn test_ergonomic_api_with_direct_values() {
let query = CallQuery::new()
.add_param("name", "test")
.add_param("age", 25)
.add_param("active", true)
.add_param("tags", vec!["rust", "web"]);
assert!(!query.is_empty());
let query_string = query.to_query_string().expect("should serialize");
insta::assert_debug_snapshot!(query_string, @r#""name=test&age=25&active=true&tags=rust&tags=web""#);
}
#[test]
fn test_mixed_ergonomic_and_explicit_api() {
let query = CallQuery::new()
.add_param("name", "test") .add_param("limit", 10) .add_param(
"tags",
ParamValue::with_style(
vec!["rust", "web"],
ParamStyle::SpaceDelimited,
),
);
let query_string = query.to_query_string().expect("should serialize");
insta::assert_debug_snapshot!(query_string, @r#""name=test&limit=10&tags=rust+web""#);
}
#[test]
fn test_query_param_as_query_value() {
let query = ParamValue::new("hello world");
let value = query.as_query_value().expect("should have value");
insta::assert_debug_snapshot!(value, @r#"String("hello world")"#);
}
#[test]
fn test_query_param_with_different_styles() {
let default_query = ParamValue::new("test");
assert_eq!(default_query.style, ParamStyle::Default);
let form_query = ParamValue::with_style("test", ParamStyle::Form);
assert_eq!(form_query.style, ParamStyle::Form);
let space_query = ParamValue::with_style("test", ParamStyle::SpaceDelimited);
assert_eq!(space_query.style, ParamStyle::SpaceDelimited);
let pipe_query = ParamValue::with_style("test", ParamStyle::PipeDelimited);
assert_eq!(pipe_query.style, ParamStyle::PipeDelimited);
}
#[test]
fn test_query_string_serialization_form_style() {
let query = CallQuery::new()
.add_param("name", ParamValue::new("john"))
.add_param("age", ParamValue::new(25));
let query_string = query.to_query_string().expect("should serialize");
insta::assert_debug_snapshot!(query_string, @r#""name=john&age=25""#);
}
#[test]
fn test_query_string_serialization_with_arrays() {
let query = CallQuery::new().add_param("tags", ParamValue::new(vec!["rust", "web", "api"]));
let query_string = query.to_query_string().expect("should serialize");
insta::assert_debug_snapshot!(query_string, @r#""tags=rust&tags=web&tags=api""#);
}
#[test]
fn test_query_string_serialization_space_delimited() {
let query = CallQuery::new().add_param(
"tags",
ParamValue::with_style(vec!["rust", "web", "api"], ParamStyle::SpaceDelimited),
);
let query_string = query.to_query_string().expect("should serialize");
insta::assert_debug_snapshot!(query_string, @r#""tags=rust+web+api""#);
}
#[test]
fn test_query_string_serialization_pipe_delimited() {
let query = CallQuery::new().add_param(
"tags",
ParamValue::with_style(vec!["rust", "web", "api"], ParamStyle::PipeDelimited),
);
let query_string = query.to_query_string().expect("should serialize");
insta::assert_debug_snapshot!(query_string, @r#""tags=rust%7Cweb%7Capi""#);
}
#[test]
fn test_empty_query_serialization() {
let query = CallQuery::new();
let query_string = query.to_query_string().expect("should serialize");
insta::assert_debug_snapshot!(query_string, @r#""""#);
}
#[test]
fn test_mixed_parameter_types() {
let query = CallQuery::new()
.add_param("name", ParamValue::new("john"))
.add_param("active", ParamValue::new(true))
.add_param("scores", ParamValue::new(vec![10, 20, 30]));
let query_string = query.to_query_string().expect("should serialize");
insta::assert_debug_snapshot!(query_string, @r#""name=john&active=true&scores=10&scores=20&scores=30""#);
}
#[test]
fn test_object_query_parameter_error() {
use serde_json::json;
let query = CallQuery::new().add_param("config", ParamValue::new(json!({"key": "value"})));
let result = query.to_query_string();
assert!(matches!(
result,
Err(ApiClientError::UnsupportedParameterValue { .. })
));
}
#[test]
fn test_nested_object_in_array_error() {
use serde_json::json;
let query = CallQuery::new().add_param(
"items",
ParamValue::new(json!(["valid", {"nested": "object"}])),
);
let result = query.to_query_string();
assert!(matches!(
result,
Err(ApiClientError::UnsupportedParameterValue { .. })
));
}
#[test]
fn test_to_parameters_generates_correct_openapi_parameters() {
let query = CallQuery::new()
.add_param("name", ParamValue::new("test"))
.add_param(
"tags",
ParamValue::with_style(vec!["a", "b"], ParamStyle::SpaceDelimited),
)
.add_param(
"limit",
ParamValue::with_style(10, ParamStyle::PipeDelimited),
);
let parameters: Vec<_> = query.to_parameters().collect();
assert_eq!(parameters.len(), 3);
for param in ¶meters {
assert_eq!(param.parameter_in, ParameterIn::Query);
assert_eq!(param.required, Required::False);
assert!(param.schema.is_some());
}
let param_names: std::collections::HashSet<_> =
parameters.iter().map(|p| p.name.as_str()).collect();
assert!(param_names.contains("name"));
assert!(param_names.contains("tags"));
assert!(param_names.contains("limit"));
}
#[test]
fn test_comprehensive_query_serialization_snapshot() {
let query = CallQuery::new()
.add_param("search", ParamValue::new("hello world"))
.add_param("active", ParamValue::new(true))
.add_param("count", ParamValue::new(42))
.add_param("tags", ParamValue::new(vec!["rust", "api", "web"]))
.add_param(
"categories",
ParamValue::with_style(vec!["tech", "programming"], ParamStyle::SpaceDelimited),
)
.add_param(
"ids",
ParamValue::with_style(vec![1, 2, 3], ParamStyle::PipeDelimited),
);
let query_string = query
.to_query_string()
.expect("serialization should succeed");
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""#);
}
#[test]
fn test_query_parameters_openapi_generation_snapshot() {
let query = CallQuery::new()
.add_param("q", ParamValue::new("search term"))
.add_param(
"filters",
ParamValue::with_style(vec!["active", "verified"], ParamStyle::SpaceDelimited),
)
.add_param(
"sort",
ParamValue::with_style(vec!["name", "date"], ParamStyle::PipeDelimited),
);
let parameters: Vec<_> = query.to_parameters().collect();
let debug_params: Vec<_> = parameters
.iter()
.map(|p| {
format!(
"{}({:?})",
p.name,
p.style
.as_ref()
.unwrap_or(&utoipa::openapi::path::ParameterStyle::Form)
)
})
.collect();
insta::assert_debug_snapshot!(debug_params, @r#"
[
"q(Form)",
"filters(SpaceDelimited)",
"sort(PipeDelimited)",
]
"#);
}
#[test]
fn test_empty_and_null_values_snapshot() {
let query = CallQuery::new()
.add_param("empty", ParamValue::new(""))
.add_param("nullable", ParamValue::new(serde_json::Value::Null));
let query_string = query
.to_query_string()
.expect("serialization should succeed");
insta::assert_debug_snapshot!(query_string, @r#""empty=&nullable=""#);
}
#[test]
fn test_special_characters_encoding_snapshot() {
let query = CallQuery::new()
.add_param("special", ParamValue::new("hello & goodbye"))
.add_param("unicode", ParamValue::new("café résumé"))
.add_param("symbols", ParamValue::new("100% guaranteed!"));
let query_string = query
.to_query_string()
.expect("serialization should succeed");
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""#);
}
#[test]
fn test_deep_object_style_with_object() {
use serde_json::json;
let query = CallQuery::new().add_param(
"user",
ParamValue::with_style(
json!({"name": "john", "age": 30, "active": true}),
ParamStyle::DeepObject,
),
);
let query_string = query
.to_query_string()
.expect("serialization should succeed");
assert!(query_string.contains("user%5Bname%5D=john"));
assert!(query_string.contains("user%5Bage%5D=30"));
assert!(query_string.contains("user%5Bactive%5D=true"));
}
#[test]
fn test_deep_object_style_with_nested_object_error() {
use serde_json::json;
let query = CallQuery::new().add_param(
"user",
ParamValue::with_style(
json!({"name": "john", "address": {"street": "123 Main St"}}),
ParamStyle::DeepObject,
),
);
let result = query.to_query_string();
assert!(matches!(
result,
Err(ApiClientError::UnsupportedParameterValue { .. })
));
}
#[test]
fn test_deep_object_style_with_array_error() {
let query = CallQuery::new().add_param(
"tags",
ParamValue::with_style(vec!["rust", "web"], ParamStyle::DeepObject),
);
let result = query.to_query_string();
assert!(matches!(
result,
Err(ApiClientError::UnsupportedParameterValue { .. })
));
}
#[test]
fn test_label_and_matrix_styles_error_in_query() {
let query =
CallQuery::new().add_param("test", ParamValue::with_style("value", ParamStyle::Label));
let result = query.to_query_string();
assert!(matches!(
result,
Err(ApiClientError::UnsupportedParameterValue { .. })
));
let query =
CallQuery::new().add_param("test", ParamValue::with_style("value", ParamStyle::Matrix));
let result = query.to_query_string();
assert!(matches!(
result,
Err(ApiClientError::UnsupportedParameterValue { .. })
));
}
}