pub(super) fn build_query_string(params: &serde_json::Value) -> String {
let Some(obj) = params.as_object() else {
return String::new();
};
let mut parts = Vec::new();
for (k, v) in obj {
let raw = match v {
serde_json::Value::Null => continue,
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
_ => match serde_json::to_string(v) {
Ok(s) => s,
Err(_) => continue,
},
};
parts.push(format!(
"{}={}",
encode_query_value(k),
encode_query_value(&raw)
));
}
parts.join("&")
}
pub(in crate::transport::rest) fn encode_query_value(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
out.push(b as char);
}
_ => {
out.push('%');
out.push(char::from(HEX_CHARS[(b >> 4) as usize]));
out.push(char::from(HEX_CHARS[(b & 0x0F) as usize]));
}
}
}
out
}
const HEX_CHARS: [u8; 16] = *b"0123456789ABCDEF";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_query_value_encodes_special_chars() {
assert_eq!(encode_query_value("hello world"), "hello%20world");
assert_eq!(encode_query_value("a=1&b=2"), "a%3D1%26b%3D2");
assert_eq!(encode_query_value("100%"), "100%25");
assert_eq!(encode_query_value("safe-._~AZaz09"), "safe-._~AZaz09");
}
#[test]
fn build_query_string_encodes_values() {
let params = serde_json::json!({
"filter": "status=active&role=admin",
"name": "John Doe"
});
let qs = build_query_string(¶ms);
assert!(qs.contains("filter=status%3Dactive%26role%3Dadmin"));
assert!(qs.contains("name=John%20Doe"));
}
#[test]
fn build_query_string_skips_null() {
let params = serde_json::json!({"a": null, "b": "hello"});
let qs = build_query_string(¶ms);
assert!(!qs.contains("a="), "null values should be skipped");
assert!(qs.contains("b=hello"), "non-null values should be present");
}
#[test]
fn build_query_string_handles_number() {
let params = serde_json::json!({"count": 42});
let qs = build_query_string(¶ms);
assert_eq!(qs, "count=42");
}
#[test]
fn build_query_string_handles_bool() {
let params = serde_json::json!({"active": true});
let qs = build_query_string(¶ms);
assert_eq!(qs, "active=true");
}
#[test]
fn build_query_string_handles_false() {
let params = serde_json::json!({"active": false});
let qs = build_query_string(¶ms);
assert_eq!(qs, "active=false");
}
#[test]
fn build_query_string_non_object_returns_empty() {
assert_eq!(build_query_string(&serde_json::json!("string")), "");
assert_eq!(build_query_string(&serde_json::json!(42)), "");
assert_eq!(build_query_string(&serde_json::json!(null)), "");
assert_eq!(build_query_string(&serde_json::json!(true)), "");
assert_eq!(build_query_string(&serde_json::json!([1, 2, 3])), "");
}
#[test]
fn build_query_string_handles_nested_object() {
let params = serde_json::json!({"data": {"key": "val"}});
let qs = build_query_string(¶ms);
assert!(qs.starts_with("data="), "should have data key: {qs}");
assert!(qs.contains("%22key%22"), "should contain encoded key: {qs}");
}
#[test]
fn build_query_string_handles_array_value() {
let params = serde_json::json!({"ids": [1, 2, 3]});
let qs = build_query_string(¶ms);
assert!(qs.starts_with("ids="), "should have ids key: {qs}");
}
#[test]
fn encode_query_value_encodes_path_traversal_chars() {
assert_eq!(encode_query_value("../admin"), "..%2Fadmin");
assert_eq!(encode_query_value("foo/bar"), "foo%2Fbar");
assert_eq!(
encode_query_value("task id with spaces"),
"task%20id%20with%20spaces"
);
}
}