use std::collections::HashMap;
use serde_json::Value;
use super::request_parameter::RequestParameter;
#[derive(Debug, Clone, Default)]
pub struct RequestData {
parameters: Vec<RequestParameter>,
}
impl RequestData {
pub fn new() -> Self {
Self::default()
}
pub fn from_parameters(parameters: Vec<RequestParameter>) -> Self {
Self { parameters }
}
pub fn push(&mut self, param: RequestParameter) {
self.parameters.push(param);
}
pub fn iter(&self) -> impl Iterator<Item = &RequestParameter> {
self.parameters.iter()
}
pub fn contains_files(&self) -> bool {
self.parameters.iter().any(|p| p.input_files.is_some())
}
pub fn parameters(&self) -> HashMap<&str, &Value> {
self.parameters
.iter()
.filter_map(|p| p.value.as_ref().map(|v| (p.name.as_ref(), v)))
.collect()
}
pub fn json_parameters(&self) -> HashMap<String, String> {
self.parameters
.iter()
.filter_map(|p| p.json_value().map(|v| (p.name.as_ref().to_owned(), v)))
.collect()
}
pub fn json_payload(&self) -> Vec<u8> {
let map = self.json_parameters();
serde_json::to_vec(&map).expect("HashMap<String,String> is always serialisable")
}
pub fn url_encoded_parameters(&self) -> String {
let map = self.json_parameters();
map.iter()
.map(|(k, v)| format!("{}={}", percent_encode(k), percent_encode(v)))
.collect::<Vec<_>>()
.join("&")
}
pub fn parametrized_url(&self, url: &str) -> String {
format!("{}?{}", url, self.url_encoded_parameters())
}
pub fn multipart_data(&self) -> Option<HashMap<String, MultipartPart>> {
if !self.contains_files() {
return None;
}
let mut out: HashMap<String, MultipartPart> = HashMap::new();
for param in &self.parameters {
if let Some(parts) = param.multipart_data() {
for (part_name, file_ref) in parts {
out.insert(
part_name,
MultipartPart {
bytes: file_ref.bytes.clone(),
mime_type: file_ref.effective_mime().to_owned(),
file_name: file_ref.file_name.clone(),
},
);
}
}
}
Some(out)
}
}
#[derive(Debug, Clone)]
pub struct MultipartPart {
pub bytes: Vec<u8>,
pub mime_type: String,
pub file_name: Option<String>,
}
fn percent_encode(input: &str) -> String {
let mut out = String::with_capacity(input.len());
for byte in input.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(byte as char)
}
_ => {
use std::fmt::Write as _;
let _ = write!(out, "%{byte:02X}");
}
}
}
out
}
#[cfg(test)]
mod tests {
use serde_json::json;
use crate::request::request_parameter::{InputFileRef, RequestParameter};
use super::*;
fn make_plain(name: &'static str, v: Value) -> RequestParameter {
RequestParameter::new(name, v)
}
#[test]
fn contains_files_false_for_plain_params() {
let data = RequestData::from_parameters(vec![
make_plain("chat_id", json!(1)),
make_plain("text", json!("hi")),
]);
assert!(!data.contains_files());
}
#[test]
fn contains_files_true_when_file_present() {
let file = InputFileRef::direct(vec![0xAB]);
let p = RequestParameter::file_only("photo", file);
let data = RequestData::from_parameters(vec![p]);
assert!(data.contains_files());
}
#[test]
fn json_parameters_excludes_none_values() {
let file = InputFileRef::direct(vec![0]);
let p = RequestParameter::file_only("photo", file);
let data = RequestData::from_parameters(vec![
make_plain("chat_id", json!(7)),
p, ]);
let jp = data.json_parameters();
assert!(jp.contains_key("chat_id"));
assert!(!jp.contains_key("photo"));
}
#[test]
fn json_parameters_string_not_double_encoded() {
let data = RequestData::from_parameters(vec![make_plain("text", json!("hello"))]);
let jp = data.json_parameters();
assert_eq!(jp["text"], "hello");
}
#[test]
fn json_payload_round_trips() {
let data = RequestData::from_parameters(vec![
make_plain("chat_id", json!(99)),
make_plain("text", json!("world")),
]);
let payload = data.json_payload();
let parsed: serde_json::Value = serde_json::from_slice(&payload).unwrap();
assert_eq!(parsed["chat_id"], json!("99"));
assert_eq!(parsed["text"], json!("world"));
}
#[test]
fn url_encoded_basic() {
let data = RequestData::from_parameters(vec![make_plain("key", json!("val"))]);
assert!(data.url_encoded_parameters().contains("key=val"));
}
#[test]
fn url_encoded_spaces_encoded_as_percent20() {
let data = RequestData::from_parameters(vec![make_plain("text", json!("hello world"))]);
let encoded = data.url_encoded_parameters();
assert!(encoded.contains("hello%20world"), "got: {encoded}");
}
#[test]
fn parametrized_url_has_question_mark() {
let data = RequestData::from_parameters(vec![make_plain("x", json!("1"))]);
let url = data.parametrized_url("https://example.com/api");
assert!(url.starts_with("https://example.com/api?"));
}
#[test]
fn multipart_data_none_without_files() {
let data = RequestData::from_parameters(vec![make_plain("chat_id", json!(1))]);
assert!(data.multipart_data().is_none());
}
#[test]
fn multipart_data_includes_file_bytes() {
let file = InputFileRef::direct(vec![0xDE, 0xAD, 0xBE, 0xEF]);
let p = RequestParameter::file_only("sticker", file);
let data = RequestData::from_parameters(vec![p]);
let parts = data.multipart_data().unwrap();
let part = parts.get("sticker").expect("part named 'sticker'");
assert_eq!(part.bytes, vec![0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn multipart_data_uses_attach_name_as_key() {
let file = InputFileRef::with_attach_name("media0", vec![0x01]);
let p = RequestParameter::with_files("media", json!("attach://media0"), vec![file]);
let data = RequestData::from_parameters(vec![p]);
let parts = data.multipart_data().unwrap();
assert!(
parts.contains_key("media0"),
"expected key 'media0', got: {parts:?}"
);
}
#[test]
fn percent_encode_unreserved_unchanged() {
assert_eq!(percent_encode("abc-_.~"), "abc-_.~");
}
#[test]
fn percent_encode_space() {
assert_eq!(percent_encode(" "), "%20");
}
#[test]
fn percent_encode_ampersand() {
assert_eq!(percent_encode("a&b"), "a%26b");
}
}